From 8f74e54a275599e1f430e380254abea35da12f7f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mikael=20=C3=96hman?= Date: Fri, 1 Oct 2021 17:47:46 +0200 Subject: [PATCH 1/3] Update hterm to 1.91 (shells) (#1426) * Update hterm to version 1.91 * Update hterm fix for undefined this.screen to 1.91 See https://github.com/OSC/ood-shell/issues/64 * Reapply patch for +-key issue #1214 on Finnish keyboard layouts See https://github.com/OSC/ondemand/issues/1214 * Add hterm bugfix to shell's README * Move hterm keycode 171 fix below second row. --- apps/shell/README.md | 1 + ..._all_1.85.mod.js => hterm_all_1.91.mod.js} | 13414 ++++++++++------ apps/shell/views/index.hbs | 2 +- 3 files changed, 8140 insertions(+), 5277 deletions(-) rename apps/shell/public/javascripts/{hterm_all_1.85.mod.js => hterm_all_1.91.mod.js} (62%) diff --git a/apps/shell/README.md b/apps/shell/README.md index 6853ac1b87..9face2fb64 100644 --- a/apps/shell/README.md +++ b/apps/shell/README.md @@ -152,6 +152,7 @@ So you've updated hterm and now something is broken. Maybe only on one platform changes. If you're lucky you may be able to cherry pick them, if not, hopefully we've made an issue where you can reference and you can at least see the commit. * 0cbc84e3d53386064e278a0495c940a217f4f18b - that fixed [issue 64](https://github.com/OSC/ood-shell/issues/64) +* a9e2e3980b0f491d0478a20e21aad022285b64ee - that fixed [issue 1214](https://github.com/OSC/ondemand/issues/1214) ## Contributing diff --git a/apps/shell/public/javascripts/hterm_all_1.85.mod.js b/apps/shell/public/javascripts/hterm_all_1.91.mod.js similarity index 62% rename from apps/shell/public/javascripts/hterm_all_1.85.mod.js rename to apps/shell/public/javascripts/hterm_all_1.91.mod.js index d986a42d7d..936ae4ca76 100644 --- a/apps/shell/public/javascripts/hterm_all_1.85.mod.js +++ b/apps/shell/public/javascripts/hterm_all_1.91.mod.js @@ -16,9 +16,39 @@ // libdot/js/lib_storage_chrome.js // libdot/js/lib_storage_local.js // libdot/js/lib_storage_memory.js -// libdot/third_party/fast-text-encoding/text.js +// libdot/js/lib_storage_terminal_private.js // libdot/third_party/intl-segmenter/intl-segmenter.js // libdot/third_party/wcwidth/lib_wc.js +// hterm/js/hterm.js +// hterm/js/hterm_accessibility_reader.js +// hterm/js/hterm_contextmenu.js +// hterm/js/hterm_find_bar.js +// hterm/js/hterm_frame.js +// hterm/js/hterm_keyboard.js +// hterm/js/hterm_keyboard_bindings.js +// hterm/js/hterm_keyboard_keymap.js +// hterm/js/hterm_keyboard_keypattern.js +// hterm/js/hterm_notifications.js +// hterm/js/hterm_options.js +// hterm/js/hterm_parser.js +// hterm/js/hterm_parser_identifiers.js +// hterm/js/hterm_preference_manager.js +// hterm/js/hterm_pubsub.js +// hterm/js/hterm_screen.js +// hterm/js/hterm_scrollport.js +// hterm/js/hterm_terminal.js +// hterm/js/hterm_terminal_io.js +// hterm/js/hterm_text_attributes.js +// hterm/js/hterm_vt.js +// hterm/js/hterm_vt_character_map.js +// hterm/audio/bell.ogg +// hterm/images/copy.svg +// hterm/images/close.svg +// hterm/images/keyboard_arrow_down.svg +// hterm/images/keyboard_arrow_up.svg +// hterm/html/find_bar.html +// hterm/html/find_screen.html +// hterm/images/icon-96.png 'use strict'; @@ -29,11 +59,6 @@ const lib = {}; -// Export for node environments. -if (typeof exports !== 'undefined') { - module.exports = lib; -} - /** * List of functions that need to be invoked during library initialization. * @@ -52,12 +77,10 @@ lib.initCallbacks_ = []; * * @param {string} name A short descriptive name of the init routine useful for * debugging. - * @param {function(function)} callback The initialization function to register. - * @return {function} The callback parameter. + * @param {function()} callback The initialization function to register. */ lib.registerInit = function(name, callback) { lib.initCallbacks_.push([name, callback]); - return callback; }; /** @@ -67,31 +90,73 @@ lib.registerInit = function(name, callback) { * invoke any registered initialization functions. * * Initialization is asynchronous. The library is not ready for use until - * the onInit function is invoked. - * - * @param {function()} onInit The function to invoke when initialization is - * complete. - * @param {function(*)} opt_logFunction An optional function to send - * initialization related log messages to. - */ -lib.init = function(onInit, opt_logFunction) { - var ary = lib.initCallbacks_; - - var initNext = function() { - if (ary.length) { - var rec = ary.shift(); - if (opt_logFunction) - opt_logFunction('init: ' + rec[0]); - rec[1](initNext); - } else { - onInit(); + * the returned promise resolves. + * + * @param {function(*)=} logFunction An optional function to send initialization + * related log messages to. + * @return {!Promise} Promise that resolves once all inits finish. + */ +lib.init = async function(logFunction = undefined) { + const ary = lib.initCallbacks_; + while (ary.length) { + const [name, init] = ary.shift(); + if (logFunction) { + logFunction(`init: ${name}`); } - }; + const ret = init(); + if (ret && typeof ret.then === 'function') { + await ret; + } + } +}; + +/** + * Verify |condition| is truthy else throw Error. + * + * This function is primarily for satisfying the JS compiler and should be + * used only when you are certain that your condition is true. The function is + * designed to have a version that throws Errors in tests if condition fails, + * and a nop version for production code. It configures itself the first time + * it runs. + * + * @param {boolean} condition A condition to check. + * @closurePrimitive {asserts.truthy} + */ +lib.assert = function(condition) { + if (window.chai) { + lib.assert = window.chai.assert; + } else { + lib.assert = function(condition) {}; + } + lib.assert(condition); +}; - if (typeof onInit != 'function') - throw new Error('Missing or invalid argument: onInit'); +/** + * Verify |value| is not null and return |value| if so, else throw Error. + * See lib.assert. + * + * @template T + * @param {T} value A value to check for null. + * @return {T} A non-null |value|. + * @closurePrimitive {asserts.truthy} + */ +lib.notNull = function(value) { + lib.assert(value !== null); + return value; +}; - setTimeout(initNext, 0); +/** + * Verify |value| is not undefined and return |value| if so, else throw Error. + * See lib.assert. + * + * @template T + * @param {T} value A value to check for null. + * @return {T} A non-undefined |value|. + * @closurePrimitive {asserts.truthy} + */ +lib.notUndefined = function(value) { + lib.assert(value !== undefined); + return value; }; // SOURCE FILE: libdot/js/lib_polyfill.js // Copyright 2017 The Chromium OS Authors. All rights reserved. @@ -99,116 +164,47 @@ lib.init = function(onInit, opt_logFunction) { // found in the LICENSE file. /** - * @fileoverview Polyfills for ES2016+ features we want to use. + * @fileoverview Polyfills for ES2019+ features we want to use. + * @suppress {duplicate} This file redefines many functions. */ -// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/padStart -if (!String.prototype.padStart) { - String.prototype.padStart = function(targetLength, padString) { - // If the string is already long enough, nothing to do! - targetLength -= this.length; - if (targetLength <= 0) - return String(this); - - if (padString === undefined) - padString = ' '; - - // In case the pad is multiple chars long. - if (targetLength > padString.length) - padString = padString.repeat((targetLength / padString.length) + 1); +/** @const */ +lib.polyfill = {}; - return padString.slice(0, targetLength) + String(this); - }; -} - -// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/padEnd -if (!String.prototype.padEnd) { - String.prototype.padEnd = function(targetLength, padString) { - // If the string is already long enough, nothing to do! - targetLength -= this.length; - if (targetLength <= 0) - return String(this); - - if (padString === undefined) - padString = ' '; - - // In case the pad is multiple chars long. - if (targetLength > padString.length) - padString = padString.repeat((targetLength / padString.length) + 1); - - return String(this) + padString.slice(0, targetLength); - }; -} - -// https://developer.mozilla.org/en/docs/Web/JavaScript/Reference/Global_objects/Object/values -// https://github.com/tc39/proposal-object-values-entries/blob/master/polyfill.js -if (!Object.values || !Object.entries) { - const reduce = Function.bind.call(Function.call, Array.prototype.reduce); - const isEnumerable = Function.bind.call(Function.call, - Object.prototype.propertyIsEnumerable); - const concat = Function.bind.call(Function.call, Array.prototype.concat); - - if (!Object.values) { - Object.values = function values(O) { - return reduce(Reflect.ownKeys(O), (v, k) => concat(v, - typeof k === 'string' && isEnumerable(O, k) ? [O[k]] : []), []); - }; - } +/** + * https://developer.mozilla.org/en-US/docs/Web/API/Blob/arrayBuffer + * + * @return {!Promise} + */ +lib.polyfill.BlobArrayBuffer = function() { + return new Promise((resolve, reject) => { + const reader = new FileReader(); + reader.onload = () => resolve(reader.result); + reader.onabort = reader.onerror = () => reject(reader); + reader.readAsArrayBuffer(this); + }); +}; - if (!Object.entries) { - Object.entries = function entries(O) { - return reduce(Reflect.ownKeys(O), (e, k) => concat(e, - typeof k === 'string' && isEnumerable(O, k) ? [[k, O[k]]] : []), []); - }; - } +if (typeof Blob.prototype.arrayBuffer != 'function') { + Blob.prototype.arrayBuffer = lib.polyfill.BlobArrayBuffer; } -// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise/finally -// https://github.com/tc39/proposal-promise-finally/blob/master/polyfill.js -if (typeof Promise.prototype.finally !== 'function') { - const speciesConstructor = function(O, defaultConstructor) { - if (!O || (typeof O !== 'object' && typeof O !== 'function')) { - throw new TypeError('Assertion failed: Type(O) is not Object'); - } - const C = O.constructor; - if (typeof C === 'undefined') { - return defaultConstructor; - } - if (!C || (typeof C !== 'object' && typeof C !== 'function')) { - throw new TypeError('O.constructor is not an Object'); - } - const S = - typeof Symbol === 'function' && typeof Symbol.species === 'symbol' ? - C[Symbol.species] : undefined; - if (S == null) { - return defaultConstructor; - } - if (typeof S === 'function' && S.prototype) { - return S; - } - throw new TypeError('no constructor found'); - }; - - const shim = { - finally(onFinally) { - const promise = this; - if (typeof promise !== 'object' || promise === null) { - throw new TypeError('"this" value is not an Object'); - } - const C = speciesConstructor(promise, Promise); - if (typeof onFinally !== 'function') { - return Promise.prototype.then.call(promise, onFinally, onFinally); - } - return Promise.prototype.then.call( - promise, - x => new C(resolve => resolve(onFinally())).then(() => x), - e => new C(resolve => resolve(onFinally())).then(() => { throw e; }) - ); - } - }; - Object.defineProperty(Promise.prototype, 'finally', { - configurable: true, writable: true, value: shim.finally, +/** + * https://developer.mozilla.org/en-US/docs/Web/API/Blob/text + * + * @return {!Promise} + */ +lib.polyfill.BlobText = function() { + return new Promise((resolve, reject) => { + const reader = new FileReader(); + reader.onload = () => resolve(reader.result); + reader.onabort = reader.onerror = () => reject(reader); + reader.readAsText(this); }); +}; + +if (typeof Blob.prototype.arrayBuffer != 'function') { + Blob.prototype.text = lib.polyfill.BlobText; } // SOURCE FILE: libdot/js/lib_array.js // Copyright 2017 The Chromium OS Authors. All rights reserved. @@ -227,7 +223,7 @@ lib.array = {}; * * @template TYPED_ARRAY * @param {...!TYPED_ARRAY} arrays - * @returns {!TYPED_ARRAY} + * @return {!TYPED_ARRAY} */ lib.array.concatTyped = function(...arrays) { let resultLength = 0; @@ -247,9 +243,9 @@ lib.array.concatTyped = function(...arrays) { * Compare two array-like objects entrywise. * * @template ARRAY_LIKE - * @param {?ARRAY_LIKE} a - * @param {?ARRAY_LIKE} b - * @returns {!boolean} true if both arrays are null or they agree entrywise; + * @param {?ARRAY_LIKE} a The first array to compare. + * @param {?ARRAY_LIKE} b The second array to compare. + * @return {boolean} true if both arrays are null or they agree entrywise; * false otherwise. */ lib.array.compare = function(a, b) { @@ -284,7 +280,8 @@ lib.codec = {}; * * The input array type may be an Array or a typed Array (e.g. Uint8Array). * - * @param {Array} array The code units to generate for the string. + * @param {!Uint8Array|!Array} array The code units to generate for + * the string. * @return {string} A UTF-16 encoded string. */ lib.codec.codeUnitArrayToString = function(array) { @@ -305,12 +302,13 @@ lib.codec.codeUnitArrayToString = function(array) { * Create an array of code units from a UTF-16 encoded string. * * @param {string} str The string to extract code units from. - * @param {type=} type The type of the return value. - * @return {Array} The array of code units. + * @param {!ArrayBufferView=} ret The buffer to hold the result. If not set, a + * new Uint8Array is created. + * @return {!ArrayBufferView} The array of code units. */ -lib.codec.stringToCodeUnitArray = function(str, type=Array) { +lib.codec.stringToCodeUnitArray = function( + str, ret = new Uint8Array(str.length)) { // Indexing string directly is faster than Array.map. - const ret = new type(str.length); for (let i = 0; i < str.length; ++i) { ret[i] = str.charCodeAt(i); } @@ -349,11 +347,11 @@ lib.colors = {}; * Instead, we stoop to this .replace() trick. */ lib.colors.re_ = { - // CSS hex color, #RGB. - hex16: /#([a-f0-9])([a-f0-9])([a-f0-9])/i, + // CSS hex color, #RGB or RGBA. + hex16: /^#([a-f0-9])([a-f0-9])([a-f0-9])([a-f0-9])?$/i, - // CSS hex color, #RRGGBB. - hex24: /#([a-f0-9]{2})([a-f0-9]{2})([a-f0-9]{2})/i, + // CSS hex color, #RRGGBB or #RRGGBBAA. + hex24: /^#([a-f0-9]{2})([a-f0-9]{2})([a-f0-9]{2})([a-f0-9]{2})?$/i, // CSS rgb color, rgb(rrr,ggg,bbb). rgb: new RegExp( @@ -361,7 +359,7 @@ lib.colors.re_ = { '/s*(/d{1,3})/s*/)/s*$' ).replace(/\//g, '\\'), 'i'), - // CSS rgb color, rgb(rrr,ggg,bbb,aaa). + // CSS rgb color, rgba(rrr,ggg,bbb,aaa). rgba: new RegExp( ('^/s*rgba/s*' + '/(/s*(/d{1,3})/s*,/s*(/d{1,3})/s*,/s*(/d{1,3})/s*' + @@ -375,6 +373,26 @@ lib.colors.re_ = { '(?:,/s*(/d+(?:/./d+)?)/s*)?/)/s*$' ).replace(/\//g, '\\'), 'i'), + // CSS hsl color, hsl(hhh,sss%,lll%). + hsl: new RegExp( + ('^/s*hsl/s*' + + '/(/s*(/d{1,3})/s*,/s*(/d{1,3})/s*%/s*,/s*(/d{1,3})/s*%/s*/)/s*$' + ).replace(/\//g, '\\'), 'i'), + + // CSS hsl color, hsla(hhh,sss%,lll%,aaa). + hsla: new RegExp( + ('^/s*hsla/s*' + + '/(/s*(/d{1,3})/s*,/s*(/d{1,3})/s*%/s*,/s*(/d{1,3})/s*%/s*' + + '(?:,/s*(/d+(?:/./d+)?)/s*)/)/s*$' + ).replace(/\//g, '\\'), 'i'), + + // Either HSL or HSLA. + hslx: new RegExp( + ('^/s*hsla?/s*' + + '/(/s*(/d{1,3})/s*,/s*(/d{1,3})/s*%/s*,/s*(/d{1,3})/s*%/s*' + + '(?:,/s*(/d+(?:/./d+)?)/s*)?/)/s*$' + ).replace(/\//g, '\\'), 'i'), + // An X11 "rgb:dddd/dddd/dddd" value. x11rgb: /^\s*rgb:([a-f0-9]{1,4})\/([a-f0-9]{1,4})\/([a-f0-9]{1,4})\s*$/i, @@ -390,7 +408,7 @@ lib.colors.re_ = { * Each 'ddd' component is a one byte value specified in decimal. * * @param {string} value The CSS color value to convert. - * @return {string} The X11 color value or null if the value could not be + * @return {?string} The X11 color value or null if the value could not be * converted. */ lib.colors.rgbToX11 = function(value) { @@ -399,15 +417,16 @@ lib.colors.rgbToX11 = function(value) { return lib.f.zpad(v, 4); } - var ary = value.match(lib.colors.re_.rgbx); - if (!ary) + const ary = value.match(lib.colors.re_.rgbx); + if (!ary) { return null; + } return 'rgb:' + scale(ary[1]) + '/' + scale(ary[2]) + '/' + scale(ary[3]); }; /** - * Convert a legacy X11 colover value into an CSS rgb(...) color value. + * Convert a legacy X11 color value into an CSS rgb(...) color value. * * They take the form: * 12 bit: #RGB -> #R000G000B000 @@ -417,26 +436,33 @@ lib.colors.rgbToX11 = function(value) { * These are the most significant bits. * * Truncate values back down to 24 bit since that's all CSS supports. + * + * @param {string} v The X11 hex color value to convert. + * @return {?string} The CSS color value or null if the value could not be + * converted. */ lib.colors.x11HexToCSS = function(v) { - if (!v.startsWith('#')) + if (!v.startsWith('#')) { return null; + } // Strip the leading # off. v = v.substr(1); // Reject unknown sizes. - if ([3, 6, 9, 12].indexOf(v.length) == -1) + if ([3, 6, 9, 12].indexOf(v.length) == -1) { return null; + } // Reject non-hex values. - if (v.match(/[^a-f0-9]/i)) + if (v.match(/[^a-f0-9]/i)) { return null; + } // Split the colors out. - var size = v.length / 3; - var r = v.substr(0, size); - var g = v.substr(size, size); - var b = v.substr(size + size, size); + const size = v.length / 3; + const r = v.substr(0, size); + const g = v.substr(size, size); + const b = v.substr(size + size, size); // Normalize to 16 bits. function norm16(v) { @@ -455,8 +481,8 @@ lib.colors.x11HexToCSS = function(v) { * rgb:hhhh/hhhh/hhhh. If a component value is less than 4 digits it is * padded out to 4, then scaled down to fit in a single byte. * - * @param {string} value The X11 color value to convert. - * @return {string} The CSS color value or null if the value could not be + * @param {string} v The X11 color value to convert. + * @return {?string} The CSS color value or null if the value could not be * converted. */ lib.colors.x11ToCSS = function(v) { @@ -491,13 +517,14 @@ lib.colors.x11ToCSS = function(v) { return Math.round(parseInt(v, 16) / 257); } - var ary = v.match(lib.colors.re_.x11rgb); + const ary = v.match(lib.colors.re_.x11rgb); if (!ary) { // Handle the legacy format. - if (v.startsWith('#')) + if (v.startsWith('#')) { return lib.colors.x11HexToCSS(v); - else + } else { return lib.colors.nameToRGB(v); + } } ary.splice(0, 1); @@ -505,121 +532,381 @@ lib.colors.x11ToCSS = function(v) { }; /** - * Converts one or more CSS '#RRGGBB' color values into their rgb(...) - * form. + * Converts one or more CSS '#RRGGBB' or '#RRGGBBAA' color values into their + * rgb(...) or rgba(...) form respectively. * * Arrays are converted in place. If a value cannot be converted, it is * replaced with null. * - * @param {string|Array.} A single RGB value or array of RGB values to - * convert. - * @return {string|Array.} The converted value or values. + * @param {string} hex A single RGB or RGBA value to convert. + * @return {?string} The converted value. */ -lib.colors.hexToRGB = function(arg) { - var hex16 = lib.colors.re_.hex16; - var hex24 = lib.colors.re_.hex24; +lib.colors.hexToRGB = function(hex) { + const hex16 = lib.colors.re_.hex16; + const hex24 = lib.colors.re_.hex24; - function convert(hex) { - if (hex.length == 4) { - hex = hex.replace(hex16, function(h, r, g, b) { - return "#" + r + r + g + g + b + b; - }); - } - var ary = hex.match(hex24); - if (!ary) - return null; + if (hex16.test(hex)) { + // Convert from RGB to RRGGBB and from RGBA to RRGGBBAA. + hex = `#${hex.match(/[a-f0-9]/gi).map((x) => `${x}${x}`).join('')}`; + } + + const ary = hex.match(hex24); + if (!ary) { + return null; + } + + const val = (index) => parseInt(ary[index + 1], 16); + return ary[4] === undefined || val(3) === 255 + ? `rgb(${val(0)}, ${val(1)}, ${val(2)})` + : `rgba(${val(0)}, ${val(1)}, ${val(2)}, ${val(3) / 255})`; +}; + +/** + * Converts one or more CSS rgb(...) or rgba(...) forms into their '#RRGGBB' or + * '#RRGGBBAA' color values respectively. + * + * Arrays are converted in place. If a value cannot be converted, it is + * replaced with null. + * + * @param {string} rgb A single rgb(...) or rgba(...) value to convert. + * @return {?string} The converted value. + */ +lib.colors.rgbToHex = function(rgb) { + const ary = lib.colors.crackRGB(rgb); + if (!ary) { + return null; + } - return 'rgb(' + parseInt(ary[1], 16) + ', ' + - parseInt(ary[2], 16) + ', ' + - parseInt(ary[3], 16) + ')'; + const hex = '#' + lib.f.zpad(( + (parseInt(ary[0], 10) << 16) | + (parseInt(ary[1], 10) << 8) | + (parseInt(ary[2], 10) << 0)).toString(16), 6); + if (ary[3] === undefined || ary[3] === '1') { + return hex; + } else { + const alpha = Math.round(255 * parseFloat(ary[3])).toString(16); + return `${hex}${lib.f.zpad(alpha, 2)}`; } +}; - if (arg instanceof Array) { - for (var i = 0; i < arg.length; i++) { - arg[i] = convert(arg[i]); +/** + * Split an hsl/hsla color into an array of its components. + * + * On success, a 4 element array will be returned. For hsl values, the alpha + * will be set to 1. + * + * @param {string} color The HSL/HSLA CSS color spec. + * @return {?Array} The HSL/HSLA values split out. + */ +lib.colors.crackHSL = function(color) { + if (color.startsWith('hsla')) { + const ary = color.match(lib.colors.re_.hsla); + if (ary) { + ary.shift(); + return Array.from(ary); } } else { - arg = convert(arg); + const ary = color.match(lib.colors.re_.hsl); + if (ary) { + ary.shift(); + ary.push('1'); + return Array.from(ary); + } } - return arg; + console.error(`Couldn't crack: ${color}`); + return null; +}; + +/** + * Converts hslx array to rgba array. + * + * The returned alpha component defaults to 1 if it isn't present in the input. + * + * The returned values are not rounded to preserve precision for computations, + * so should be rounded before they are used in CSS strings. + * + * @param {?Array} hslx The HSL or HSLA elements to convert. + * @return {!Array} The RGBA values. + */ +lib.colors.hslxArrayToRgbaArray = function(hslx) { + const hue = parseInt(hslx[0], 10) / 60; + const sat = parseInt(hslx[1], 10) / 100; + const light = parseInt(hslx[2], 10) / 100; + + // The following algorithm has been adapted from: + // https://www.w3.org/TR/css-color-4/#hsl-to-rgb + const hueToRgb = (t1, t2, hue) => { + if (hue < 0) { + hue += 6; + } + if (hue >= 6) { + hue -= 6; + } + + if (hue < 1) { + return (t2 - t1) * hue + t1; + } else if (hue < 3) { + return t2; + } else if (hue < 4) { + return (t2 - t1) * (4 - hue) + t1; + } else { + return t1; + } + }; + + const t2 = light <= 0.5 ? light * (sat + 1) : light + sat - (light * sat); + const t1 = light * 2 - t2; + + return [ + 255 * hueToRgb(t1, t2, hue + 2), + 255 * hueToRgb(t1, t2, hue), + 255 * hueToRgb(t1, t2, hue - 2), + hslx[3] !== undefined ? +hslx[3] : 1, + ]; +}; + +/** + * Converts a hsvx array to a hsla array. The hsvx array is an array of [hue + * (>=0, <=360), saturation (>=0, <=100), value (>=0, <=100), alpha] (alpha can + * be missing). + * + * The returned alpha component defaults to 1 if it isn't present in the input. + * + * The returned values are not rounded to preserve precision for computations, + * so should be rounded before they are used in CSS strings. + * + * @param {?Array} hsvx The hsv or hsva array. + * @return {!Array} The hsla array. + */ +lib.colors.hsvxArrayToHslaArray = function(hsvx) { + const clamp = (x) => lib.f.clamp(x, 0, 100); + const [hue, saturation, value] = hsvx.map(parseFloat); + const hslLightness = clamp(value * (100 - saturation / 2) / 100); + let hslSaturation = 0; + if (hslLightness !== 0 && hslLightness !== 100) { + hslSaturation = clamp((value - hslLightness) / + Math.min(hslLightness, 100 - hslLightness) * 100); + } + return [ + hue, + hslSaturation, + hslLightness, + hsvx.length === 4 ? +hsvx[3] : 1, + ]; }; /** - * Converts one or more CSS rgb(...) forms into their '#RRGGBB' color values. + * Converts a hslx array to a hsva array. The hsva array is an array of [hue + * (>=0, <=360), saturation (>=0, <=100), value (>=0, <=100), alpha]. * - * If given an rgba(...) form, the alpha field is thrown away. + * The returned alpha component defaults to 1 if it isn't present in the input. + * + * @param {?Array} hslx The hsl or hsla array. + * @return {!Array} The hsva array. + */ +lib.colors.hslxArrayToHsvaArray = function(hslx) { + const clamp = (x) => lib.f.clamp(x, 0, 100); + const [hue, saturation, lightness] = hslx.map(parseFloat); + const hsvValue = clamp( + lightness + saturation * Math.min(lightness, 100 - lightness) / 100); + let hsvSaturation = 0; + if (hsvValue !== 0) { + hsvSaturation = clamp(200 * (1 - lightness / hsvValue)); + } + return [hue, hsvSaturation, hsvValue, hslx.length === 4 ? +hslx[3] : 1]; +}; + +/** + * Converts one or more CSS hsl(...) or hsla(...) forms into their rgb(...) or + * rgba(...) color values respectively. * * Arrays are converted in place. If a value cannot be converted, it is * replaced with null. * - * @param {string|Array.} A single rgb(...) value or array of rgb(...) - * values to convert. - * @return {string|Array.} The converted value or values. + * @param {string} hsl A single hsl(...) or hsla(...) value to convert. + * @return {?string} The converted value. + */ +lib.colors.hslToRGB = function(hsl) { + const ary = lib.colors.crackHSL(hsl); + if (!ary) { + return null; + } + + const [r, g, b, a] = lib.colors.hslxArrayToRgbaArray(ary); + + const rgb = [r, g, b].map(Math.round).join(', '); + + return a === 1 ? `rgb(${rgb})` : `rgba(${rgb}, ${a})`; +}; + +/** + * Converts rgbx array to hsla array. + * + * The returned alpha component defaults to 1 if it isn't present in the input. + * + * The returned values are not rounded to preserve precision for computations, + * so should be rounded before they are used in CSS strings. + * + * @param {?Array} rgbx The RGB or RGBA elements to convert. + * @return {!Array} The HSLA values. */ -lib.colors.rgbToHex = function(arg) { - function convert(rgb) { - var ary = lib.colors.crackRGB(rgb); - if (!ary) - return null; - return '#' + lib.f.zpad(((parseInt(ary[0]) << 16) | - (parseInt(ary[1]) << 8) | - (parseInt(ary[2]) << 0)).toString(16), 6); +lib.colors.rgbxArrayToHslaArray = function(rgbx) { + const r = parseInt(rgbx[0], 10) / 255; + const g = parseInt(rgbx[1], 10) / 255; + const b = parseInt(rgbx[2], 10) / 255; + + const min = Math.min(r, g, b); + const max = Math.max(r, g, b); + const spread = max - min; + + /* eslint-disable id-blacklist */ + const l = (max + min) / 2; + + if (spread == 0) { + return [0, 0, 100 * l, rgbx[3] !== undefined ? +rgbx[3] : 1]; } - if (arg instanceof Array) { - for (var i = 0; i < arg.length; i++) { - arg[i] = convert(arg[i]); + let h = (() => { + switch (max) { + case r: return ((g - b) / spread) % 6; + case g: return (b - r) / spread + 2; + case b: return (r - g) / spread + 4; } - } else { - arg = convert(arg); + })(); + h *= 60; + if (h < 0) { + h += 360; + } + + const s = spread / (1 - Math.abs(2 * l - 1)); + + return [h, 100 * s, 100 * l, rgbx[3] !== undefined ? +rgbx[3] : 1]; + /* eslint-enable id-blacklist */ +}; + +/** + * Converts one or more CSS rgb(...) or rgba(...) forms into their hsl(...) or + * hsla(...) color values respectively. + * + * Arrays are converted in place. If a value cannot be converted, it is + * replaced with null. + * + * @param {string} rgb A single rgb(...) or rgba(...) value to convert. + * @return {?string} The converted value. + */ +lib.colors.rgbToHsl = function(rgb) { + const ary = lib.colors.crackRGB(rgb); + if (!ary) { + return null; } - return arg; + /* eslint-disable id-blacklist */ + // eslint-disable-next-line prefer-const + let [h, s, l, a] = lib.colors.rgbxArrayToHslaArray(ary); + h = Math.round(h); + s = Math.round(s); + l = Math.round(l); + + return a === 1 ? `hsl(${h}, ${s}%, ${l}%)` : `hsla(${h}, ${s}%, ${l}%, ${a})`; + /* eslint-enable id-blacklist */ }; /** - * Take any valid css color definition and turn it into an rgb or rgba value. + * Take any valid CSS color definition and turn it into an rgb or rgba value. * - * Returns null if the value could not be normalized. + * @param {string} def The CSS color spec to normalize. + * @return {?string} The converted value. */ lib.colors.normalizeCSS = function(def) { - if (def.startsWith('#')) + if (def.startsWith('#')) { return lib.colors.hexToRGB(def); + } - if (lib.colors.re_.rgbx.test(def)) + if (lib.colors.re_.rgbx.test(def)) { return def; + } + + if (lib.colors.re_.hslx.test(def)) { + return lib.colors.hslToRGB(def); + } return lib.colors.nameToRGB(def); }; /** - * Convert a 3 or 4 element array into an rgba(...) string. + * Take any valid CSS color definition and turn it into an hsl or hsla value. + * + * @param {string} def The CSS color spec to normalize. + * @return {?string} The converted value. + */ +lib.colors.normalizeCSSToHSL = function(def) { + if (lib.colors.re_.hslx.test(def)) { + return def; + } + + const rgb = lib.colors.normalizeCSS(def); + if (!rgb) { + return rgb; + } + return lib.colors.rgbToHsl(rgb); +}; + +/** + * Convert a 3 or 4 element array into an rgb(...) or rgba(...) string. + * + * @param {?Array} ary The RGB or RGBA elements to convert. + * @return {string} The normalized CSS color spec. */ lib.colors.arrayToRGBA = function(ary) { - var alpha = (ary.length > 3) ? ary[3] : 1; - return 'rgba(' + ary[0] + ', ' + ary[1] + ', ' + ary[2] + ', ' + alpha + ')'; + if (ary.length == 3) { + return `rgb(${ary[0]}, ${ary[1]}, ${ary[2]})`; + } + return `rgba(${ary[0]}, ${ary[1]}, ${ary[2]}, ${ary[3]})`; +}; + +/** + * Convert a 3 or 4 element array into an hsla(...) string. + * + * @param {?Array} ary The HSL or HSLA elements to convert. + * @return {string} The normalized CSS color spec. + */ +lib.colors.arrayToHSLA = function(ary) { + const alpha = (ary.length > 3) ? ary[3] : 1; + return `hsla(${Math.round(ary[0])}, ${Math.round(ary[1])}%, ` + + `${Math.round(ary[2])}%, ${alpha})`; }; /** * Overwrite the alpha channel of an rgb/rgba color. + * + * @param {string} rgb The normalized CSS color spec. + * @param {number} alpha The alpha channel. + * @return {string} The normalized CSS color spec with updated alpha channel. */ lib.colors.setAlpha = function(rgb, alpha) { - var ary = lib.colors.crackRGB(rgb); - ary[3] = alpha; + const ary = lib.colors.crackRGB(rgb); + ary[3] = alpha.toString(); return lib.colors.arrayToRGBA(ary); }; /** * Mix a percentage of a tint color into a base color. + * + * @param {string} base The normalized CSS base color spec. + * @param {string} tint The normalized CSS color to tint with. + * @param {number} percent The percentage of the tinting. + * @return {string} The new tinted CSS color spec. */ lib.colors.mix = function(base, tint, percent) { - var ary1 = lib.colors.crackRGB(base); - var ary2 = lib.colors.crackRGB(tint); + const ary1 = lib.colors.crackRGB(base); + const ary2 = lib.colors.crackRGB(tint); - for (var i = 0; i < 4; ++i) { - var diff = ary2[i] - ary1[i]; - ary1[i] = Math.round(parseInt(ary1[i]) + diff * percent); + for (let i = 0; i < 4; ++i) { + const basecol = parseInt(ary1[i], 10); + const tintcol = parseInt(ary2[i], 10); + const diff = tintcol - basecol; + ary1[i] = Math.round(base + diff * percent).toString(); } return lib.colors.arrayToRGBA(ary1); @@ -630,16 +917,19 @@ lib.colors.mix = function(base, tint, percent) { * * On success, a 4 element array will be returned. For rgb values, the alpha * will be set to 1. + * + * @param {string} color The RGB/RGBA CSS color spec. + * @return {?Array} The RGB/RGBA values split out. */ lib.colors.crackRGB = function(color) { if (color.startsWith('rgba')) { - var ary = color.match(lib.colors.re_.rgba); + const ary = color.match(lib.colors.re_.rgba); if (ary) { ary.shift(); return Array.from(ary); } } else { - var ary = color.match(lib.colors.re_.rgb); + const ary = color.match(lib.colors.re_.rgb); if (ary) { ary.shift(); ary.push('1'); @@ -661,28 +951,67 @@ lib.colors.crackRGB = function(color) { * rgb.txt file. * * @param {string} name The color name to convert. - * @return {string} The corresponding CSS rgb(...) value. + * @return {?string} The corresponding CSS rgb(...) value. */ lib.colors.nameToRGB = function(name) { - if (name in lib.colors.colorNames) + if (name in lib.colors.colorNames) { return lib.colors.colorNames[name]; + } name = name.toLowerCase(); - if (name in lib.colors.colorNames) + if (name in lib.colors.colorNames) { return lib.colors.colorNames[name]; + } name = name.replace(/\s+/g, ''); - if (name in lib.colors.colorNames) + if (name in lib.colors.colorNames) { return lib.colors.colorNames[name]; + } return null; }; +/** + * Calculate the relative luminance as per + * https://www.w3.org/TR/WCAG20/#relativeluminancedef + * + * @param {number} r The value (>=0 and <= 255) of the rgb component. + * @param {number} g The value (>=0 and <= 255) of the rgb component. + * @param {number} b The value (>=0 and <= 255) of the rgb component. + * @return {number} The relative luminance. + */ +lib.colors.luminance = function(r, g, b) { + const [rr, gg, bb] = [r, g, b].map((value) => { + value /= 255; + if (value <= 0.03928) { + return value / 12.92; + } else { + return Math.pow((value + 0.055) / 1.055, 2.4); + } + }); + + return 0.2126 * rr + 0.7152 * gg + 0.0722 * bb; +}; + +/** + * Calculate the contrast ratio of two relative luminance values as per + * https://www.w3.org/TR/WCAG20/#contrast-ratiodef + * + * @param {number} l1 Relative luminance value. + * @param {number} l2 Relative luminance value. + * @return {number} The contrast ratio. + */ +lib.colors.contrastRatio = function(l1, l2) { + return (Math.max(l1, l2) + 0.05) / (Math.min(l1, l2) + 0.05); +}; + /** * The stock color palette. + * + * @type {!Array} */ -lib.colors.stockColorPalette = lib.colors.hexToRGB - ([// The "ANSI 16"... +lib.colors.stockPalette = [ + // The "ANSI 16"... '#000000', '#CC0000', '#4E9A06', '#C4A000', '#3465A4', '#75507B', '#06989A', '#D3D7CF', '#555753', '#EF2929', '#00BA13', '#FCE94F', @@ -735,676 +1064,671 @@ lib.colors.stockColorPalette = lib.colors.hexToRGB '#080808', '#121212', '#1C1C1C', '#262626', '#303030', '#3A3A3A', '#444444', '#4E4E4E', '#585858', '#626262', '#6C6C6C', '#767676', '#808080', '#8A8A8A', '#949494', '#9E9E9E', '#A8A8A8', '#B2B2B2', - '#BCBCBC', '#C6C6C6', '#D0D0D0', '#DADADA', '#E4E4E4', '#EEEEEE' - ]); - -/** - * The current color palette, possibly with user changes. - */ -lib.colors.colorPalette = lib.colors.stockColorPalette; + '#BCBCBC', '#C6C6C6', '#D0D0D0', '#DADADA', '#E4E4E4', '#EEEEEE', + ].map(lib.colors.hexToRGB); /** * Named colors according to the stock X11 rgb.txt file. */ lib.colors.colorNames = { - "aliceblue": "rgb(240, 248, 255)", - "antiquewhite": "rgb(250, 235, 215)", - "antiquewhite1": "rgb(255, 239, 219)", - "antiquewhite2": "rgb(238, 223, 204)", - "antiquewhite3": "rgb(205, 192, 176)", - "antiquewhite4": "rgb(139, 131, 120)", - "aquamarine": "rgb(127, 255, 212)", - "aquamarine1": "rgb(127, 255, 212)", - "aquamarine2": "rgb(118, 238, 198)", - "aquamarine3": "rgb(102, 205, 170)", - "aquamarine4": "rgb(69, 139, 116)", - "azure": "rgb(240, 255, 255)", - "azure1": "rgb(240, 255, 255)", - "azure2": "rgb(224, 238, 238)", - "azure3": "rgb(193, 205, 205)", - "azure4": "rgb(131, 139, 139)", - "beige": "rgb(245, 245, 220)", - "bisque": "rgb(255, 228, 196)", - "bisque1": "rgb(255, 228, 196)", - "bisque2": "rgb(238, 213, 183)", - "bisque3": "rgb(205, 183, 158)", - "bisque4": "rgb(139, 125, 107)", - "black": "rgb(0, 0, 0)", - "blanchedalmond": "rgb(255, 235, 205)", - "blue": "rgb(0, 0, 255)", - "blue1": "rgb(0, 0, 255)", - "blue2": "rgb(0, 0, 238)", - "blue3": "rgb(0, 0, 205)", - "blue4": "rgb(0, 0, 139)", - "blueviolet": "rgb(138, 43, 226)", - "brown": "rgb(165, 42, 42)", - "brown1": "rgb(255, 64, 64)", - "brown2": "rgb(238, 59, 59)", - "brown3": "rgb(205, 51, 51)", - "brown4": "rgb(139, 35, 35)", - "burlywood": "rgb(222, 184, 135)", - "burlywood1": "rgb(255, 211, 155)", - "burlywood2": "rgb(238, 197, 145)", - "burlywood3": "rgb(205, 170, 125)", - "burlywood4": "rgb(139, 115, 85)", - "cadetblue": "rgb(95, 158, 160)", - "cadetblue1": "rgb(152, 245, 255)", - "cadetblue2": "rgb(142, 229, 238)", - "cadetblue3": "rgb(122, 197, 205)", - "cadetblue4": "rgb(83, 134, 139)", - "chartreuse": "rgb(127, 255, 0)", - "chartreuse1": "rgb(127, 255, 0)", - "chartreuse2": "rgb(118, 238, 0)", - "chartreuse3": "rgb(102, 205, 0)", - "chartreuse4": "rgb(69, 139, 0)", - "chocolate": "rgb(210, 105, 30)", - "chocolate1": "rgb(255, 127, 36)", - "chocolate2": "rgb(238, 118, 33)", - "chocolate3": "rgb(205, 102, 29)", - "chocolate4": "rgb(139, 69, 19)", - "coral": "rgb(255, 127, 80)", - "coral1": "rgb(255, 114, 86)", - "coral2": "rgb(238, 106, 80)", - "coral3": "rgb(205, 91, 69)", - "coral4": "rgb(139, 62, 47)", - "cornflowerblue": "rgb(100, 149, 237)", - "cornsilk": "rgb(255, 248, 220)", - "cornsilk1": "rgb(255, 248, 220)", - "cornsilk2": "rgb(238, 232, 205)", - "cornsilk3": "rgb(205, 200, 177)", - "cornsilk4": "rgb(139, 136, 120)", - "cyan": "rgb(0, 255, 255)", - "cyan1": "rgb(0, 255, 255)", - "cyan2": "rgb(0, 238, 238)", - "cyan3": "rgb(0, 205, 205)", - "cyan4": "rgb(0, 139, 139)", - "darkblue": "rgb(0, 0, 139)", - "darkcyan": "rgb(0, 139, 139)", - "darkgoldenrod": "rgb(184, 134, 11)", - "darkgoldenrod1": "rgb(255, 185, 15)", - "darkgoldenrod2": "rgb(238, 173, 14)", - "darkgoldenrod3": "rgb(205, 149, 12)", - "darkgoldenrod4": "rgb(139, 101, 8)", - "darkgray": "rgb(169, 169, 169)", - "darkgreen": "rgb(0, 100, 0)", - "darkgrey": "rgb(169, 169, 169)", - "darkkhaki": "rgb(189, 183, 107)", - "darkmagenta": "rgb(139, 0, 139)", - "darkolivegreen": "rgb(85, 107, 47)", - "darkolivegreen1": "rgb(202, 255, 112)", - "darkolivegreen2": "rgb(188, 238, 104)", - "darkolivegreen3": "rgb(162, 205, 90)", - "darkolivegreen4": "rgb(110, 139, 61)", - "darkorange": "rgb(255, 140, 0)", - "darkorange1": "rgb(255, 127, 0)", - "darkorange2": "rgb(238, 118, 0)", - "darkorange3": "rgb(205, 102, 0)", - "darkorange4": "rgb(139, 69, 0)", - "darkorchid": "rgb(153, 50, 204)", - "darkorchid1": "rgb(191, 62, 255)", - "darkorchid2": "rgb(178, 58, 238)", - "darkorchid3": "rgb(154, 50, 205)", - "darkorchid4": "rgb(104, 34, 139)", - "darkred": "rgb(139, 0, 0)", - "darksalmon": "rgb(233, 150, 122)", - "darkseagreen": "rgb(143, 188, 143)", - "darkseagreen1": "rgb(193, 255, 193)", - "darkseagreen2": "rgb(180, 238, 180)", - "darkseagreen3": "rgb(155, 205, 155)", - "darkseagreen4": "rgb(105, 139, 105)", - "darkslateblue": "rgb(72, 61, 139)", - "darkslategray": "rgb(47, 79, 79)", - "darkslategray1": "rgb(151, 255, 255)", - "darkslategray2": "rgb(141, 238, 238)", - "darkslategray3": "rgb(121, 205, 205)", - "darkslategray4": "rgb(82, 139, 139)", - "darkslategrey": "rgb(47, 79, 79)", - "darkturquoise": "rgb(0, 206, 209)", - "darkviolet": "rgb(148, 0, 211)", - "debianred": "rgb(215, 7, 81)", - "deeppink": "rgb(255, 20, 147)", - "deeppink1": "rgb(255, 20, 147)", - "deeppink2": "rgb(238, 18, 137)", - "deeppink3": "rgb(205, 16, 118)", - "deeppink4": "rgb(139, 10, 80)", - "deepskyblue": "rgb(0, 191, 255)", - "deepskyblue1": "rgb(0, 191, 255)", - "deepskyblue2": "rgb(0, 178, 238)", - "deepskyblue3": "rgb(0, 154, 205)", - "deepskyblue4": "rgb(0, 104, 139)", - "dimgray": "rgb(105, 105, 105)", - "dimgrey": "rgb(105, 105, 105)", - "dodgerblue": "rgb(30, 144, 255)", - "dodgerblue1": "rgb(30, 144, 255)", - "dodgerblue2": "rgb(28, 134, 238)", - "dodgerblue3": "rgb(24, 116, 205)", - "dodgerblue4": "rgb(16, 78, 139)", - "firebrick": "rgb(178, 34, 34)", - "firebrick1": "rgb(255, 48, 48)", - "firebrick2": "rgb(238, 44, 44)", - "firebrick3": "rgb(205, 38, 38)", - "firebrick4": "rgb(139, 26, 26)", - "floralwhite": "rgb(255, 250, 240)", - "forestgreen": "rgb(34, 139, 34)", - "gainsboro": "rgb(220, 220, 220)", - "ghostwhite": "rgb(248, 248, 255)", - "gold": "rgb(255, 215, 0)", - "gold1": "rgb(255, 215, 0)", - "gold2": "rgb(238, 201, 0)", - "gold3": "rgb(205, 173, 0)", - "gold4": "rgb(139, 117, 0)", - "goldenrod": "rgb(218, 165, 32)", - "goldenrod1": "rgb(255, 193, 37)", - "goldenrod2": "rgb(238, 180, 34)", - "goldenrod3": "rgb(205, 155, 29)", - "goldenrod4": "rgb(139, 105, 20)", - "gray": "rgb(190, 190, 190)", - "gray0": "rgb(0, 0, 0)", - "gray1": "rgb(3, 3, 3)", - "gray10": "rgb(26, 26, 26)", - "gray100": "rgb(255, 255, 255)", - "gray11": "rgb(28, 28, 28)", - "gray12": "rgb(31, 31, 31)", - "gray13": "rgb(33, 33, 33)", - "gray14": "rgb(36, 36, 36)", - "gray15": "rgb(38, 38, 38)", - "gray16": "rgb(41, 41, 41)", - "gray17": "rgb(43, 43, 43)", - "gray18": "rgb(46, 46, 46)", - "gray19": "rgb(48, 48, 48)", - "gray2": "rgb(5, 5, 5)", - "gray20": "rgb(51, 51, 51)", - "gray21": "rgb(54, 54, 54)", - "gray22": "rgb(56, 56, 56)", - "gray23": "rgb(59, 59, 59)", - "gray24": "rgb(61, 61, 61)", - "gray25": "rgb(64, 64, 64)", - "gray26": "rgb(66, 66, 66)", - "gray27": "rgb(69, 69, 69)", - "gray28": "rgb(71, 71, 71)", - "gray29": "rgb(74, 74, 74)", - "gray3": "rgb(8, 8, 8)", - "gray30": "rgb(77, 77, 77)", - "gray31": "rgb(79, 79, 79)", - "gray32": "rgb(82, 82, 82)", - "gray33": "rgb(84, 84, 84)", - "gray34": "rgb(87, 87, 87)", - "gray35": "rgb(89, 89, 89)", - "gray36": "rgb(92, 92, 92)", - "gray37": "rgb(94, 94, 94)", - "gray38": "rgb(97, 97, 97)", - "gray39": "rgb(99, 99, 99)", - "gray4": "rgb(10, 10, 10)", - "gray40": "rgb(102, 102, 102)", - "gray41": "rgb(105, 105, 105)", - "gray42": "rgb(107, 107, 107)", - "gray43": "rgb(110, 110, 110)", - "gray44": "rgb(112, 112, 112)", - "gray45": "rgb(115, 115, 115)", - "gray46": "rgb(117, 117, 117)", - "gray47": "rgb(120, 120, 120)", - "gray48": "rgb(122, 122, 122)", - "gray49": "rgb(125, 125, 125)", - "gray5": "rgb(13, 13, 13)", - "gray50": "rgb(127, 127, 127)", - "gray51": "rgb(130, 130, 130)", - "gray52": "rgb(133, 133, 133)", - "gray53": "rgb(135, 135, 135)", - "gray54": "rgb(138, 138, 138)", - "gray55": "rgb(140, 140, 140)", - "gray56": "rgb(143, 143, 143)", - "gray57": "rgb(145, 145, 145)", - "gray58": "rgb(148, 148, 148)", - "gray59": "rgb(150, 150, 150)", - "gray6": "rgb(15, 15, 15)", - "gray60": "rgb(153, 153, 153)", - "gray61": "rgb(156, 156, 156)", - "gray62": "rgb(158, 158, 158)", - "gray63": "rgb(161, 161, 161)", - "gray64": "rgb(163, 163, 163)", - "gray65": "rgb(166, 166, 166)", - "gray66": "rgb(168, 168, 168)", - "gray67": "rgb(171, 171, 171)", - "gray68": "rgb(173, 173, 173)", - "gray69": "rgb(176, 176, 176)", - "gray7": "rgb(18, 18, 18)", - "gray70": "rgb(179, 179, 179)", - "gray71": "rgb(181, 181, 181)", - "gray72": "rgb(184, 184, 184)", - "gray73": "rgb(186, 186, 186)", - "gray74": "rgb(189, 189, 189)", - "gray75": "rgb(191, 191, 191)", - "gray76": "rgb(194, 194, 194)", - "gray77": "rgb(196, 196, 196)", - "gray78": "rgb(199, 199, 199)", - "gray79": "rgb(201, 201, 201)", - "gray8": "rgb(20, 20, 20)", - "gray80": "rgb(204, 204, 204)", - "gray81": "rgb(207, 207, 207)", - "gray82": "rgb(209, 209, 209)", - "gray83": "rgb(212, 212, 212)", - "gray84": "rgb(214, 214, 214)", - "gray85": "rgb(217, 217, 217)", - "gray86": "rgb(219, 219, 219)", - "gray87": "rgb(222, 222, 222)", - "gray88": "rgb(224, 224, 224)", - "gray89": "rgb(227, 227, 227)", - "gray9": "rgb(23, 23, 23)", - "gray90": "rgb(229, 229, 229)", - "gray91": "rgb(232, 232, 232)", - "gray92": "rgb(235, 235, 235)", - "gray93": "rgb(237, 237, 237)", - "gray94": "rgb(240, 240, 240)", - "gray95": "rgb(242, 242, 242)", - "gray96": "rgb(245, 245, 245)", - "gray97": "rgb(247, 247, 247)", - "gray98": "rgb(250, 250, 250)", - "gray99": "rgb(252, 252, 252)", - "green": "rgb(0, 255, 0)", - "green1": "rgb(0, 255, 0)", - "green2": "rgb(0, 238, 0)", - "green3": "rgb(0, 205, 0)", - "green4": "rgb(0, 139, 0)", - "greenyellow": "rgb(173, 255, 47)", - "grey": "rgb(190, 190, 190)", - "grey0": "rgb(0, 0, 0)", - "grey1": "rgb(3, 3, 3)", - "grey10": "rgb(26, 26, 26)", - "grey100": "rgb(255, 255, 255)", - "grey11": "rgb(28, 28, 28)", - "grey12": "rgb(31, 31, 31)", - "grey13": "rgb(33, 33, 33)", - "grey14": "rgb(36, 36, 36)", - "grey15": "rgb(38, 38, 38)", - "grey16": "rgb(41, 41, 41)", - "grey17": "rgb(43, 43, 43)", - "grey18": "rgb(46, 46, 46)", - "grey19": "rgb(48, 48, 48)", - "grey2": "rgb(5, 5, 5)", - "grey20": "rgb(51, 51, 51)", - "grey21": "rgb(54, 54, 54)", - "grey22": "rgb(56, 56, 56)", - "grey23": "rgb(59, 59, 59)", - "grey24": "rgb(61, 61, 61)", - "grey25": "rgb(64, 64, 64)", - "grey26": "rgb(66, 66, 66)", - "grey27": "rgb(69, 69, 69)", - "grey28": "rgb(71, 71, 71)", - "grey29": "rgb(74, 74, 74)", - "grey3": "rgb(8, 8, 8)", - "grey30": "rgb(77, 77, 77)", - "grey31": "rgb(79, 79, 79)", - "grey32": "rgb(82, 82, 82)", - "grey33": "rgb(84, 84, 84)", - "grey34": "rgb(87, 87, 87)", - "grey35": "rgb(89, 89, 89)", - "grey36": "rgb(92, 92, 92)", - "grey37": "rgb(94, 94, 94)", - "grey38": "rgb(97, 97, 97)", - "grey39": "rgb(99, 99, 99)", - "grey4": "rgb(10, 10, 10)", - "grey40": "rgb(102, 102, 102)", - "grey41": "rgb(105, 105, 105)", - "grey42": "rgb(107, 107, 107)", - "grey43": "rgb(110, 110, 110)", - "grey44": "rgb(112, 112, 112)", - "grey45": "rgb(115, 115, 115)", - "grey46": "rgb(117, 117, 117)", - "grey47": "rgb(120, 120, 120)", - "grey48": "rgb(122, 122, 122)", - "grey49": "rgb(125, 125, 125)", - "grey5": "rgb(13, 13, 13)", - "grey50": "rgb(127, 127, 127)", - "grey51": "rgb(130, 130, 130)", - "grey52": "rgb(133, 133, 133)", - "grey53": "rgb(135, 135, 135)", - "grey54": "rgb(138, 138, 138)", - "grey55": "rgb(140, 140, 140)", - "grey56": "rgb(143, 143, 143)", - "grey57": "rgb(145, 145, 145)", - "grey58": "rgb(148, 148, 148)", - "grey59": "rgb(150, 150, 150)", - "grey6": "rgb(15, 15, 15)", - "grey60": "rgb(153, 153, 153)", - "grey61": "rgb(156, 156, 156)", - "grey62": "rgb(158, 158, 158)", - "grey63": "rgb(161, 161, 161)", - "grey64": "rgb(163, 163, 163)", - "grey65": "rgb(166, 166, 166)", - "grey66": "rgb(168, 168, 168)", - "grey67": "rgb(171, 171, 171)", - "grey68": "rgb(173, 173, 173)", - "grey69": "rgb(176, 176, 176)", - "grey7": "rgb(18, 18, 18)", - "grey70": "rgb(179, 179, 179)", - "grey71": "rgb(181, 181, 181)", - "grey72": "rgb(184, 184, 184)", - "grey73": "rgb(186, 186, 186)", - "grey74": "rgb(189, 189, 189)", - "grey75": "rgb(191, 191, 191)", - "grey76": "rgb(194, 194, 194)", - "grey77": "rgb(196, 196, 196)", - "grey78": "rgb(199, 199, 199)", - "grey79": "rgb(201, 201, 201)", - "grey8": "rgb(20, 20, 20)", - "grey80": "rgb(204, 204, 204)", - "grey81": "rgb(207, 207, 207)", - "grey82": "rgb(209, 209, 209)", - "grey83": "rgb(212, 212, 212)", - "grey84": "rgb(214, 214, 214)", - "grey85": "rgb(217, 217, 217)", - "grey86": "rgb(219, 219, 219)", - "grey87": "rgb(222, 222, 222)", - "grey88": "rgb(224, 224, 224)", - "grey89": "rgb(227, 227, 227)", - "grey9": "rgb(23, 23, 23)", - "grey90": "rgb(229, 229, 229)", - "grey91": "rgb(232, 232, 232)", - "grey92": "rgb(235, 235, 235)", - "grey93": "rgb(237, 237, 237)", - "grey94": "rgb(240, 240, 240)", - "grey95": "rgb(242, 242, 242)", - "grey96": "rgb(245, 245, 245)", - "grey97": "rgb(247, 247, 247)", - "grey98": "rgb(250, 250, 250)", - "grey99": "rgb(252, 252, 252)", - "honeydew": "rgb(240, 255, 240)", - "honeydew1": "rgb(240, 255, 240)", - "honeydew2": "rgb(224, 238, 224)", - "honeydew3": "rgb(193, 205, 193)", - "honeydew4": "rgb(131, 139, 131)", - "hotpink": "rgb(255, 105, 180)", - "hotpink1": "rgb(255, 110, 180)", - "hotpink2": "rgb(238, 106, 167)", - "hotpink3": "rgb(205, 96, 144)", - "hotpink4": "rgb(139, 58, 98)", - "indianred": "rgb(205, 92, 92)", - "indianred1": "rgb(255, 106, 106)", - "indianred2": "rgb(238, 99, 99)", - "indianred3": "rgb(205, 85, 85)", - "indianred4": "rgb(139, 58, 58)", - "ivory": "rgb(255, 255, 240)", - "ivory1": "rgb(255, 255, 240)", - "ivory2": "rgb(238, 238, 224)", - "ivory3": "rgb(205, 205, 193)", - "ivory4": "rgb(139, 139, 131)", - "khaki": "rgb(240, 230, 140)", - "khaki1": "rgb(255, 246, 143)", - "khaki2": "rgb(238, 230, 133)", - "khaki3": "rgb(205, 198, 115)", - "khaki4": "rgb(139, 134, 78)", - "lavender": "rgb(230, 230, 250)", - "lavenderblush": "rgb(255, 240, 245)", - "lavenderblush1": "rgb(255, 240, 245)", - "lavenderblush2": "rgb(238, 224, 229)", - "lavenderblush3": "rgb(205, 193, 197)", - "lavenderblush4": "rgb(139, 131, 134)", - "lawngreen": "rgb(124, 252, 0)", - "lemonchiffon": "rgb(255, 250, 205)", - "lemonchiffon1": "rgb(255, 250, 205)", - "lemonchiffon2": "rgb(238, 233, 191)", - "lemonchiffon3": "rgb(205, 201, 165)", - "lemonchiffon4": "rgb(139, 137, 112)", - "lightblue": "rgb(173, 216, 230)", - "lightblue1": "rgb(191, 239, 255)", - "lightblue2": "rgb(178, 223, 238)", - "lightblue3": "rgb(154, 192, 205)", - "lightblue4": "rgb(104, 131, 139)", - "lightcoral": "rgb(240, 128, 128)", - "lightcyan": "rgb(224, 255, 255)", - "lightcyan1": "rgb(224, 255, 255)", - "lightcyan2": "rgb(209, 238, 238)", - "lightcyan3": "rgb(180, 205, 205)", - "lightcyan4": "rgb(122, 139, 139)", - "lightgoldenrod": "rgb(238, 221, 130)", - "lightgoldenrod1": "rgb(255, 236, 139)", - "lightgoldenrod2": "rgb(238, 220, 130)", - "lightgoldenrod3": "rgb(205, 190, 112)", - "lightgoldenrod4": "rgb(139, 129, 76)", - "lightgoldenrodyellow": "rgb(250, 250, 210)", - "lightgray": "rgb(211, 211, 211)", - "lightgreen": "rgb(144, 238, 144)", - "lightgrey": "rgb(211, 211, 211)", - "lightpink": "rgb(255, 182, 193)", - "lightpink1": "rgb(255, 174, 185)", - "lightpink2": "rgb(238, 162, 173)", - "lightpink3": "rgb(205, 140, 149)", - "lightpink4": "rgb(139, 95, 101)", - "lightsalmon": "rgb(255, 160, 122)", - "lightsalmon1": "rgb(255, 160, 122)", - "lightsalmon2": "rgb(238, 149, 114)", - "lightsalmon3": "rgb(205, 129, 98)", - "lightsalmon4": "rgb(139, 87, 66)", - "lightseagreen": "rgb(32, 178, 170)", - "lightskyblue": "rgb(135, 206, 250)", - "lightskyblue1": "rgb(176, 226, 255)", - "lightskyblue2": "rgb(164, 211, 238)", - "lightskyblue3": "rgb(141, 182, 205)", - "lightskyblue4": "rgb(96, 123, 139)", - "lightslateblue": "rgb(132, 112, 255)", - "lightslategray": "rgb(119, 136, 153)", - "lightslategrey": "rgb(119, 136, 153)", - "lightsteelblue": "rgb(176, 196, 222)", - "lightsteelblue1": "rgb(202, 225, 255)", - "lightsteelblue2": "rgb(188, 210, 238)", - "lightsteelblue3": "rgb(162, 181, 205)", - "lightsteelblue4": "rgb(110, 123, 139)", - "lightyellow": "rgb(255, 255, 224)", - "lightyellow1": "rgb(255, 255, 224)", - "lightyellow2": "rgb(238, 238, 209)", - "lightyellow3": "rgb(205, 205, 180)", - "lightyellow4": "rgb(139, 139, 122)", - "limegreen": "rgb(50, 205, 50)", - "linen": "rgb(250, 240, 230)", - "magenta": "rgb(255, 0, 255)", - "magenta1": "rgb(255, 0, 255)", - "magenta2": "rgb(238, 0, 238)", - "magenta3": "rgb(205, 0, 205)", - "magenta4": "rgb(139, 0, 139)", - "maroon": "rgb(176, 48, 96)", - "maroon1": "rgb(255, 52, 179)", - "maroon2": "rgb(238, 48, 167)", - "maroon3": "rgb(205, 41, 144)", - "maroon4": "rgb(139, 28, 98)", - "mediumaquamarine": "rgb(102, 205, 170)", - "mediumblue": "rgb(0, 0, 205)", - "mediumorchid": "rgb(186, 85, 211)", - "mediumorchid1": "rgb(224, 102, 255)", - "mediumorchid2": "rgb(209, 95, 238)", - "mediumorchid3": "rgb(180, 82, 205)", - "mediumorchid4": "rgb(122, 55, 139)", - "mediumpurple": "rgb(147, 112, 219)", - "mediumpurple1": "rgb(171, 130, 255)", - "mediumpurple2": "rgb(159, 121, 238)", - "mediumpurple3": "rgb(137, 104, 205)", - "mediumpurple4": "rgb(93, 71, 139)", - "mediumseagreen": "rgb(60, 179, 113)", - "mediumslateblue": "rgb(123, 104, 238)", - "mediumspringgreen": "rgb(0, 250, 154)", - "mediumturquoise": "rgb(72, 209, 204)", - "mediumvioletred": "rgb(199, 21, 133)", - "midnightblue": "rgb(25, 25, 112)", - "mintcream": "rgb(245, 255, 250)", - "mistyrose": "rgb(255, 228, 225)", - "mistyrose1": "rgb(255, 228, 225)", - "mistyrose2": "rgb(238, 213, 210)", - "mistyrose3": "rgb(205, 183, 181)", - "mistyrose4": "rgb(139, 125, 123)", - "moccasin": "rgb(255, 228, 181)", - "navajowhite": "rgb(255, 222, 173)", - "navajowhite1": "rgb(255, 222, 173)", - "navajowhite2": "rgb(238, 207, 161)", - "navajowhite3": "rgb(205, 179, 139)", - "navajowhite4": "rgb(139, 121, 94)", - "navy": "rgb(0, 0, 128)", - "navyblue": "rgb(0, 0, 128)", - "oldlace": "rgb(253, 245, 230)", - "olivedrab": "rgb(107, 142, 35)", - "olivedrab1": "rgb(192, 255, 62)", - "olivedrab2": "rgb(179, 238, 58)", - "olivedrab3": "rgb(154, 205, 50)", - "olivedrab4": "rgb(105, 139, 34)", - "orange": "rgb(255, 165, 0)", - "orange1": "rgb(255, 165, 0)", - "orange2": "rgb(238, 154, 0)", - "orange3": "rgb(205, 133, 0)", - "orange4": "rgb(139, 90, 0)", - "orangered": "rgb(255, 69, 0)", - "orangered1": "rgb(255, 69, 0)", - "orangered2": "rgb(238, 64, 0)", - "orangered3": "rgb(205, 55, 0)", - "orangered4": "rgb(139, 37, 0)", - "orchid": "rgb(218, 112, 214)", - "orchid1": "rgb(255, 131, 250)", - "orchid2": "rgb(238, 122, 233)", - "orchid3": "rgb(205, 105, 201)", - "orchid4": "rgb(139, 71, 137)", - "palegoldenrod": "rgb(238, 232, 170)", - "palegreen": "rgb(152, 251, 152)", - "palegreen1": "rgb(154, 255, 154)", - "palegreen2": "rgb(144, 238, 144)", - "palegreen3": "rgb(124, 205, 124)", - "palegreen4": "rgb(84, 139, 84)", - "paleturquoise": "rgb(175, 238, 238)", - "paleturquoise1": "rgb(187, 255, 255)", - "paleturquoise2": "rgb(174, 238, 238)", - "paleturquoise3": "rgb(150, 205, 205)", - "paleturquoise4": "rgb(102, 139, 139)", - "palevioletred": "rgb(219, 112, 147)", - "palevioletred1": "rgb(255, 130, 171)", - "palevioletred2": "rgb(238, 121, 159)", - "palevioletred3": "rgb(205, 104, 137)", - "palevioletred4": "rgb(139, 71, 93)", - "papayawhip": "rgb(255, 239, 213)", - "peachpuff": "rgb(255, 218, 185)", - "peachpuff1": "rgb(255, 218, 185)", - "peachpuff2": "rgb(238, 203, 173)", - "peachpuff3": "rgb(205, 175, 149)", - "peachpuff4": "rgb(139, 119, 101)", - "peru": "rgb(205, 133, 63)", - "pink": "rgb(255, 192, 203)", - "pink1": "rgb(255, 181, 197)", - "pink2": "rgb(238, 169, 184)", - "pink3": "rgb(205, 145, 158)", - "pink4": "rgb(139, 99, 108)", - "plum": "rgb(221, 160, 221)", - "plum1": "rgb(255, 187, 255)", - "plum2": "rgb(238, 174, 238)", - "plum3": "rgb(205, 150, 205)", - "plum4": "rgb(139, 102, 139)", - "powderblue": "rgb(176, 224, 230)", - "purple": "rgb(160, 32, 240)", - "purple1": "rgb(155, 48, 255)", - "purple2": "rgb(145, 44, 238)", - "purple3": "rgb(125, 38, 205)", - "purple4": "rgb(85, 26, 139)", - "red": "rgb(255, 0, 0)", - "red1": "rgb(255, 0, 0)", - "red2": "rgb(238, 0, 0)", - "red3": "rgb(205, 0, 0)", - "red4": "rgb(139, 0, 0)", - "rosybrown": "rgb(188, 143, 143)", - "rosybrown1": "rgb(255, 193, 193)", - "rosybrown2": "rgb(238, 180, 180)", - "rosybrown3": "rgb(205, 155, 155)", - "rosybrown4": "rgb(139, 105, 105)", - "royalblue": "rgb(65, 105, 225)", - "royalblue1": "rgb(72, 118, 255)", - "royalblue2": "rgb(67, 110, 238)", - "royalblue3": "rgb(58, 95, 205)", - "royalblue4": "rgb(39, 64, 139)", - "saddlebrown": "rgb(139, 69, 19)", - "salmon": "rgb(250, 128, 114)", - "salmon1": "rgb(255, 140, 105)", - "salmon2": "rgb(238, 130, 98)", - "salmon3": "rgb(205, 112, 84)", - "salmon4": "rgb(139, 76, 57)", - "sandybrown": "rgb(244, 164, 96)", - "seagreen": "rgb(46, 139, 87)", - "seagreen1": "rgb(84, 255, 159)", - "seagreen2": "rgb(78, 238, 148)", - "seagreen3": "rgb(67, 205, 128)", - "seagreen4": "rgb(46, 139, 87)", - "seashell": "rgb(255, 245, 238)", - "seashell1": "rgb(255, 245, 238)", - "seashell2": "rgb(238, 229, 222)", - "seashell3": "rgb(205, 197, 191)", - "seashell4": "rgb(139, 134, 130)", - "sienna": "rgb(160, 82, 45)", - "sienna1": "rgb(255, 130, 71)", - "sienna2": "rgb(238, 121, 66)", - "sienna3": "rgb(205, 104, 57)", - "sienna4": "rgb(139, 71, 38)", - "skyblue": "rgb(135, 206, 235)", - "skyblue1": "rgb(135, 206, 255)", - "skyblue2": "rgb(126, 192, 238)", - "skyblue3": "rgb(108, 166, 205)", - "skyblue4": "rgb(74, 112, 139)", - "slateblue": "rgb(106, 90, 205)", - "slateblue1": "rgb(131, 111, 255)", - "slateblue2": "rgb(122, 103, 238)", - "slateblue3": "rgb(105, 89, 205)", - "slateblue4": "rgb(71, 60, 139)", - "slategray": "rgb(112, 128, 144)", - "slategray1": "rgb(198, 226, 255)", - "slategray2": "rgb(185, 211, 238)", - "slategray3": "rgb(159, 182, 205)", - "slategray4": "rgb(108, 123, 139)", - "slategrey": "rgb(112, 128, 144)", - "snow": "rgb(255, 250, 250)", - "snow1": "rgb(255, 250, 250)", - "snow2": "rgb(238, 233, 233)", - "snow3": "rgb(205, 201, 201)", - "snow4": "rgb(139, 137, 137)", - "springgreen": "rgb(0, 255, 127)", - "springgreen1": "rgb(0, 255, 127)", - "springgreen2": "rgb(0, 238, 118)", - "springgreen3": "rgb(0, 205, 102)", - "springgreen4": "rgb(0, 139, 69)", - "steelblue": "rgb(70, 130, 180)", - "steelblue1": "rgb(99, 184, 255)", - "steelblue2": "rgb(92, 172, 238)", - "steelblue3": "rgb(79, 148, 205)", - "steelblue4": "rgb(54, 100, 139)", - "tan": "rgb(210, 180, 140)", - "tan1": "rgb(255, 165, 79)", - "tan2": "rgb(238, 154, 73)", - "tan3": "rgb(205, 133, 63)", - "tan4": "rgb(139, 90, 43)", - "thistle": "rgb(216, 191, 216)", - "thistle1": "rgb(255, 225, 255)", - "thistle2": "rgb(238, 210, 238)", - "thistle3": "rgb(205, 181, 205)", - "thistle4": "rgb(139, 123, 139)", - "tomato": "rgb(255, 99, 71)", - "tomato1": "rgb(255, 99, 71)", - "tomato2": "rgb(238, 92, 66)", - "tomato3": "rgb(205, 79, 57)", - "tomato4": "rgb(139, 54, 38)", - "turquoise": "rgb(64, 224, 208)", - "turquoise1": "rgb(0, 245, 255)", - "turquoise2": "rgb(0, 229, 238)", - "turquoise3": "rgb(0, 197, 205)", - "turquoise4": "rgb(0, 134, 139)", - "violet": "rgb(238, 130, 238)", - "violetred": "rgb(208, 32, 144)", - "violetred1": "rgb(255, 62, 150)", - "violetred2": "rgb(238, 58, 140)", - "violetred3": "rgb(205, 50, 120)", - "violetred4": "rgb(139, 34, 82)", - "wheat": "rgb(245, 222, 179)", - "wheat1": "rgb(255, 231, 186)", - "wheat2": "rgb(238, 216, 174)", - "wheat3": "rgb(205, 186, 150)", - "wheat4": "rgb(139, 126, 102)", - "white": "rgb(255, 255, 255)", - "whitesmoke": "rgb(245, 245, 245)", - "yellow": "rgb(255, 255, 0)", - "yellow1": "rgb(255, 255, 0)", - "yellow2": "rgb(238, 238, 0)", - "yellow3": "rgb(205, 205, 0)", - "yellow4": "rgb(139, 139, 0)", - "yellowgreen": "rgb(154, 205, 50)" + 'aliceblue': 'rgb(240, 248, 255)', + 'antiquewhite': 'rgb(250, 235, 215)', + 'antiquewhite1': 'rgb(255, 239, 219)', + 'antiquewhite2': 'rgb(238, 223, 204)', + 'antiquewhite3': 'rgb(205, 192, 176)', + 'antiquewhite4': 'rgb(139, 131, 120)', + 'aquamarine': 'rgb(127, 255, 212)', + 'aquamarine1': 'rgb(127, 255, 212)', + 'aquamarine2': 'rgb(118, 238, 198)', + 'aquamarine3': 'rgb(102, 205, 170)', + 'aquamarine4': 'rgb(69, 139, 116)', + 'azure': 'rgb(240, 255, 255)', + 'azure1': 'rgb(240, 255, 255)', + 'azure2': 'rgb(224, 238, 238)', + 'azure3': 'rgb(193, 205, 205)', + 'azure4': 'rgb(131, 139, 139)', + 'beige': 'rgb(245, 245, 220)', + 'bisque': 'rgb(255, 228, 196)', + 'bisque1': 'rgb(255, 228, 196)', + 'bisque2': 'rgb(238, 213, 183)', + 'bisque3': 'rgb(205, 183, 158)', + 'bisque4': 'rgb(139, 125, 107)', + 'black': 'rgb(0, 0, 0)', + 'blanchedalmond': 'rgb(255, 235, 205)', + 'blue': 'rgb(0, 0, 255)', + 'blue1': 'rgb(0, 0, 255)', + 'blue2': 'rgb(0, 0, 238)', + 'blue3': 'rgb(0, 0, 205)', + 'blue4': 'rgb(0, 0, 139)', + 'blueviolet': 'rgb(138, 43, 226)', + 'brown': 'rgb(165, 42, 42)', + 'brown1': 'rgb(255, 64, 64)', + 'brown2': 'rgb(238, 59, 59)', + 'brown3': 'rgb(205, 51, 51)', + 'brown4': 'rgb(139, 35, 35)', + 'burlywood': 'rgb(222, 184, 135)', + 'burlywood1': 'rgb(255, 211, 155)', + 'burlywood2': 'rgb(238, 197, 145)', + 'burlywood3': 'rgb(205, 170, 125)', + 'burlywood4': 'rgb(139, 115, 85)', + 'cadetblue': 'rgb(95, 158, 160)', + 'cadetblue1': 'rgb(152, 245, 255)', + 'cadetblue2': 'rgb(142, 229, 238)', + 'cadetblue3': 'rgb(122, 197, 205)', + 'cadetblue4': 'rgb(83, 134, 139)', + 'chartreuse': 'rgb(127, 255, 0)', + 'chartreuse1': 'rgb(127, 255, 0)', + 'chartreuse2': 'rgb(118, 238, 0)', + 'chartreuse3': 'rgb(102, 205, 0)', + 'chartreuse4': 'rgb(69, 139, 0)', + 'chocolate': 'rgb(210, 105, 30)', + 'chocolate1': 'rgb(255, 127, 36)', + 'chocolate2': 'rgb(238, 118, 33)', + 'chocolate3': 'rgb(205, 102, 29)', + 'chocolate4': 'rgb(139, 69, 19)', + 'coral': 'rgb(255, 127, 80)', + 'coral1': 'rgb(255, 114, 86)', + 'coral2': 'rgb(238, 106, 80)', + 'coral3': 'rgb(205, 91, 69)', + 'coral4': 'rgb(139, 62, 47)', + 'cornflowerblue': 'rgb(100, 149, 237)', + 'cornsilk': 'rgb(255, 248, 220)', + 'cornsilk1': 'rgb(255, 248, 220)', + 'cornsilk2': 'rgb(238, 232, 205)', + 'cornsilk3': 'rgb(205, 200, 177)', + 'cornsilk4': 'rgb(139, 136, 120)', + 'cyan': 'rgb(0, 255, 255)', + 'cyan1': 'rgb(0, 255, 255)', + 'cyan2': 'rgb(0, 238, 238)', + 'cyan3': 'rgb(0, 205, 205)', + 'cyan4': 'rgb(0, 139, 139)', + 'darkblue': 'rgb(0, 0, 139)', + 'darkcyan': 'rgb(0, 139, 139)', + 'darkgoldenrod': 'rgb(184, 134, 11)', + 'darkgoldenrod1': 'rgb(255, 185, 15)', + 'darkgoldenrod2': 'rgb(238, 173, 14)', + 'darkgoldenrod3': 'rgb(205, 149, 12)', + 'darkgoldenrod4': 'rgb(139, 101, 8)', + 'darkgray': 'rgb(169, 169, 169)', + 'darkgreen': 'rgb(0, 100, 0)', + 'darkgrey': 'rgb(169, 169, 169)', + 'darkkhaki': 'rgb(189, 183, 107)', + 'darkmagenta': 'rgb(139, 0, 139)', + 'darkolivegreen': 'rgb(85, 107, 47)', + 'darkolivegreen1': 'rgb(202, 255, 112)', + 'darkolivegreen2': 'rgb(188, 238, 104)', + 'darkolivegreen3': 'rgb(162, 205, 90)', + 'darkolivegreen4': 'rgb(110, 139, 61)', + 'darkorange': 'rgb(255, 140, 0)', + 'darkorange1': 'rgb(255, 127, 0)', + 'darkorange2': 'rgb(238, 118, 0)', + 'darkorange3': 'rgb(205, 102, 0)', + 'darkorange4': 'rgb(139, 69, 0)', + 'darkorchid': 'rgb(153, 50, 204)', + 'darkorchid1': 'rgb(191, 62, 255)', + 'darkorchid2': 'rgb(178, 58, 238)', + 'darkorchid3': 'rgb(154, 50, 205)', + 'darkorchid4': 'rgb(104, 34, 139)', + 'darkred': 'rgb(139, 0, 0)', + 'darksalmon': 'rgb(233, 150, 122)', + 'darkseagreen': 'rgb(143, 188, 143)', + 'darkseagreen1': 'rgb(193, 255, 193)', + 'darkseagreen2': 'rgb(180, 238, 180)', + 'darkseagreen3': 'rgb(155, 205, 155)', + 'darkseagreen4': 'rgb(105, 139, 105)', + 'darkslateblue': 'rgb(72, 61, 139)', + 'darkslategray': 'rgb(47, 79, 79)', + 'darkslategray1': 'rgb(151, 255, 255)', + 'darkslategray2': 'rgb(141, 238, 238)', + 'darkslategray3': 'rgb(121, 205, 205)', + 'darkslategray4': 'rgb(82, 139, 139)', + 'darkslategrey': 'rgb(47, 79, 79)', + 'darkturquoise': 'rgb(0, 206, 209)', + 'darkviolet': 'rgb(148, 0, 211)', + 'debianred': 'rgb(215, 7, 81)', + 'deeppink': 'rgb(255, 20, 147)', + 'deeppink1': 'rgb(255, 20, 147)', + 'deeppink2': 'rgb(238, 18, 137)', + 'deeppink3': 'rgb(205, 16, 118)', + 'deeppink4': 'rgb(139, 10, 80)', + 'deepskyblue': 'rgb(0, 191, 255)', + 'deepskyblue1': 'rgb(0, 191, 255)', + 'deepskyblue2': 'rgb(0, 178, 238)', + 'deepskyblue3': 'rgb(0, 154, 205)', + 'deepskyblue4': 'rgb(0, 104, 139)', + 'dimgray': 'rgb(105, 105, 105)', + 'dimgrey': 'rgb(105, 105, 105)', + 'dodgerblue': 'rgb(30, 144, 255)', + 'dodgerblue1': 'rgb(30, 144, 255)', + 'dodgerblue2': 'rgb(28, 134, 238)', + 'dodgerblue3': 'rgb(24, 116, 205)', + 'dodgerblue4': 'rgb(16, 78, 139)', + 'firebrick': 'rgb(178, 34, 34)', + 'firebrick1': 'rgb(255, 48, 48)', + 'firebrick2': 'rgb(238, 44, 44)', + 'firebrick3': 'rgb(205, 38, 38)', + 'firebrick4': 'rgb(139, 26, 26)', + 'floralwhite': 'rgb(255, 250, 240)', + 'forestgreen': 'rgb(34, 139, 34)', + 'gainsboro': 'rgb(220, 220, 220)', + 'ghostwhite': 'rgb(248, 248, 255)', + 'gold': 'rgb(255, 215, 0)', + 'gold1': 'rgb(255, 215, 0)', + 'gold2': 'rgb(238, 201, 0)', + 'gold3': 'rgb(205, 173, 0)', + 'gold4': 'rgb(139, 117, 0)', + 'goldenrod': 'rgb(218, 165, 32)', + 'goldenrod1': 'rgb(255, 193, 37)', + 'goldenrod2': 'rgb(238, 180, 34)', + 'goldenrod3': 'rgb(205, 155, 29)', + 'goldenrod4': 'rgb(139, 105, 20)', + 'gray': 'rgb(190, 190, 190)', + 'gray0': 'rgb(0, 0, 0)', + 'gray1': 'rgb(3, 3, 3)', + 'gray10': 'rgb(26, 26, 26)', + 'gray100': 'rgb(255, 255, 255)', + 'gray11': 'rgb(28, 28, 28)', + 'gray12': 'rgb(31, 31, 31)', + 'gray13': 'rgb(33, 33, 33)', + 'gray14': 'rgb(36, 36, 36)', + 'gray15': 'rgb(38, 38, 38)', + 'gray16': 'rgb(41, 41, 41)', + 'gray17': 'rgb(43, 43, 43)', + 'gray18': 'rgb(46, 46, 46)', + 'gray19': 'rgb(48, 48, 48)', + 'gray2': 'rgb(5, 5, 5)', + 'gray20': 'rgb(51, 51, 51)', + 'gray21': 'rgb(54, 54, 54)', + 'gray22': 'rgb(56, 56, 56)', + 'gray23': 'rgb(59, 59, 59)', + 'gray24': 'rgb(61, 61, 61)', + 'gray25': 'rgb(64, 64, 64)', + 'gray26': 'rgb(66, 66, 66)', + 'gray27': 'rgb(69, 69, 69)', + 'gray28': 'rgb(71, 71, 71)', + 'gray29': 'rgb(74, 74, 74)', + 'gray3': 'rgb(8, 8, 8)', + 'gray30': 'rgb(77, 77, 77)', + 'gray31': 'rgb(79, 79, 79)', + 'gray32': 'rgb(82, 82, 82)', + 'gray33': 'rgb(84, 84, 84)', + 'gray34': 'rgb(87, 87, 87)', + 'gray35': 'rgb(89, 89, 89)', + 'gray36': 'rgb(92, 92, 92)', + 'gray37': 'rgb(94, 94, 94)', + 'gray38': 'rgb(97, 97, 97)', + 'gray39': 'rgb(99, 99, 99)', + 'gray4': 'rgb(10, 10, 10)', + 'gray40': 'rgb(102, 102, 102)', + 'gray41': 'rgb(105, 105, 105)', + 'gray42': 'rgb(107, 107, 107)', + 'gray43': 'rgb(110, 110, 110)', + 'gray44': 'rgb(112, 112, 112)', + 'gray45': 'rgb(115, 115, 115)', + 'gray46': 'rgb(117, 117, 117)', + 'gray47': 'rgb(120, 120, 120)', + 'gray48': 'rgb(122, 122, 122)', + 'gray49': 'rgb(125, 125, 125)', + 'gray5': 'rgb(13, 13, 13)', + 'gray50': 'rgb(127, 127, 127)', + 'gray51': 'rgb(130, 130, 130)', + 'gray52': 'rgb(133, 133, 133)', + 'gray53': 'rgb(135, 135, 135)', + 'gray54': 'rgb(138, 138, 138)', + 'gray55': 'rgb(140, 140, 140)', + 'gray56': 'rgb(143, 143, 143)', + 'gray57': 'rgb(145, 145, 145)', + 'gray58': 'rgb(148, 148, 148)', + 'gray59': 'rgb(150, 150, 150)', + 'gray6': 'rgb(15, 15, 15)', + 'gray60': 'rgb(153, 153, 153)', + 'gray61': 'rgb(156, 156, 156)', + 'gray62': 'rgb(158, 158, 158)', + 'gray63': 'rgb(161, 161, 161)', + 'gray64': 'rgb(163, 163, 163)', + 'gray65': 'rgb(166, 166, 166)', + 'gray66': 'rgb(168, 168, 168)', + 'gray67': 'rgb(171, 171, 171)', + 'gray68': 'rgb(173, 173, 173)', + 'gray69': 'rgb(176, 176, 176)', + 'gray7': 'rgb(18, 18, 18)', + 'gray70': 'rgb(179, 179, 179)', + 'gray71': 'rgb(181, 181, 181)', + 'gray72': 'rgb(184, 184, 184)', + 'gray73': 'rgb(186, 186, 186)', + 'gray74': 'rgb(189, 189, 189)', + 'gray75': 'rgb(191, 191, 191)', + 'gray76': 'rgb(194, 194, 194)', + 'gray77': 'rgb(196, 196, 196)', + 'gray78': 'rgb(199, 199, 199)', + 'gray79': 'rgb(201, 201, 201)', + 'gray8': 'rgb(20, 20, 20)', + 'gray80': 'rgb(204, 204, 204)', + 'gray81': 'rgb(207, 207, 207)', + 'gray82': 'rgb(209, 209, 209)', + 'gray83': 'rgb(212, 212, 212)', + 'gray84': 'rgb(214, 214, 214)', + 'gray85': 'rgb(217, 217, 217)', + 'gray86': 'rgb(219, 219, 219)', + 'gray87': 'rgb(222, 222, 222)', + 'gray88': 'rgb(224, 224, 224)', + 'gray89': 'rgb(227, 227, 227)', + 'gray9': 'rgb(23, 23, 23)', + 'gray90': 'rgb(229, 229, 229)', + 'gray91': 'rgb(232, 232, 232)', + 'gray92': 'rgb(235, 235, 235)', + 'gray93': 'rgb(237, 237, 237)', + 'gray94': 'rgb(240, 240, 240)', + 'gray95': 'rgb(242, 242, 242)', + 'gray96': 'rgb(245, 245, 245)', + 'gray97': 'rgb(247, 247, 247)', + 'gray98': 'rgb(250, 250, 250)', + 'gray99': 'rgb(252, 252, 252)', + 'green': 'rgb(0, 255, 0)', + 'green1': 'rgb(0, 255, 0)', + 'green2': 'rgb(0, 238, 0)', + 'green3': 'rgb(0, 205, 0)', + 'green4': 'rgb(0, 139, 0)', + 'greenyellow': 'rgb(173, 255, 47)', + 'grey': 'rgb(190, 190, 190)', + 'grey0': 'rgb(0, 0, 0)', + 'grey1': 'rgb(3, 3, 3)', + 'grey10': 'rgb(26, 26, 26)', + 'grey100': 'rgb(255, 255, 255)', + 'grey11': 'rgb(28, 28, 28)', + 'grey12': 'rgb(31, 31, 31)', + 'grey13': 'rgb(33, 33, 33)', + 'grey14': 'rgb(36, 36, 36)', + 'grey15': 'rgb(38, 38, 38)', + 'grey16': 'rgb(41, 41, 41)', + 'grey17': 'rgb(43, 43, 43)', + 'grey18': 'rgb(46, 46, 46)', + 'grey19': 'rgb(48, 48, 48)', + 'grey2': 'rgb(5, 5, 5)', + 'grey20': 'rgb(51, 51, 51)', + 'grey21': 'rgb(54, 54, 54)', + 'grey22': 'rgb(56, 56, 56)', + 'grey23': 'rgb(59, 59, 59)', + 'grey24': 'rgb(61, 61, 61)', + 'grey25': 'rgb(64, 64, 64)', + 'grey26': 'rgb(66, 66, 66)', + 'grey27': 'rgb(69, 69, 69)', + 'grey28': 'rgb(71, 71, 71)', + 'grey29': 'rgb(74, 74, 74)', + 'grey3': 'rgb(8, 8, 8)', + 'grey30': 'rgb(77, 77, 77)', + 'grey31': 'rgb(79, 79, 79)', + 'grey32': 'rgb(82, 82, 82)', + 'grey33': 'rgb(84, 84, 84)', + 'grey34': 'rgb(87, 87, 87)', + 'grey35': 'rgb(89, 89, 89)', + 'grey36': 'rgb(92, 92, 92)', + 'grey37': 'rgb(94, 94, 94)', + 'grey38': 'rgb(97, 97, 97)', + 'grey39': 'rgb(99, 99, 99)', + 'grey4': 'rgb(10, 10, 10)', + 'grey40': 'rgb(102, 102, 102)', + 'grey41': 'rgb(105, 105, 105)', + 'grey42': 'rgb(107, 107, 107)', + 'grey43': 'rgb(110, 110, 110)', + 'grey44': 'rgb(112, 112, 112)', + 'grey45': 'rgb(115, 115, 115)', + 'grey46': 'rgb(117, 117, 117)', + 'grey47': 'rgb(120, 120, 120)', + 'grey48': 'rgb(122, 122, 122)', + 'grey49': 'rgb(125, 125, 125)', + 'grey5': 'rgb(13, 13, 13)', + 'grey50': 'rgb(127, 127, 127)', + 'grey51': 'rgb(130, 130, 130)', + 'grey52': 'rgb(133, 133, 133)', + 'grey53': 'rgb(135, 135, 135)', + 'grey54': 'rgb(138, 138, 138)', + 'grey55': 'rgb(140, 140, 140)', + 'grey56': 'rgb(143, 143, 143)', + 'grey57': 'rgb(145, 145, 145)', + 'grey58': 'rgb(148, 148, 148)', + 'grey59': 'rgb(150, 150, 150)', + 'grey6': 'rgb(15, 15, 15)', + 'grey60': 'rgb(153, 153, 153)', + 'grey61': 'rgb(156, 156, 156)', + 'grey62': 'rgb(158, 158, 158)', + 'grey63': 'rgb(161, 161, 161)', + 'grey64': 'rgb(163, 163, 163)', + 'grey65': 'rgb(166, 166, 166)', + 'grey66': 'rgb(168, 168, 168)', + 'grey67': 'rgb(171, 171, 171)', + 'grey68': 'rgb(173, 173, 173)', + 'grey69': 'rgb(176, 176, 176)', + 'grey7': 'rgb(18, 18, 18)', + 'grey70': 'rgb(179, 179, 179)', + 'grey71': 'rgb(181, 181, 181)', + 'grey72': 'rgb(184, 184, 184)', + 'grey73': 'rgb(186, 186, 186)', + 'grey74': 'rgb(189, 189, 189)', + 'grey75': 'rgb(191, 191, 191)', + 'grey76': 'rgb(194, 194, 194)', + 'grey77': 'rgb(196, 196, 196)', + 'grey78': 'rgb(199, 199, 199)', + 'grey79': 'rgb(201, 201, 201)', + 'grey8': 'rgb(20, 20, 20)', + 'grey80': 'rgb(204, 204, 204)', + 'grey81': 'rgb(207, 207, 207)', + 'grey82': 'rgb(209, 209, 209)', + 'grey83': 'rgb(212, 212, 212)', + 'grey84': 'rgb(214, 214, 214)', + 'grey85': 'rgb(217, 217, 217)', + 'grey86': 'rgb(219, 219, 219)', + 'grey87': 'rgb(222, 222, 222)', + 'grey88': 'rgb(224, 224, 224)', + 'grey89': 'rgb(227, 227, 227)', + 'grey9': 'rgb(23, 23, 23)', + 'grey90': 'rgb(229, 229, 229)', + 'grey91': 'rgb(232, 232, 232)', + 'grey92': 'rgb(235, 235, 235)', + 'grey93': 'rgb(237, 237, 237)', + 'grey94': 'rgb(240, 240, 240)', + 'grey95': 'rgb(242, 242, 242)', + 'grey96': 'rgb(245, 245, 245)', + 'grey97': 'rgb(247, 247, 247)', + 'grey98': 'rgb(250, 250, 250)', + 'grey99': 'rgb(252, 252, 252)', + 'honeydew': 'rgb(240, 255, 240)', + 'honeydew1': 'rgb(240, 255, 240)', + 'honeydew2': 'rgb(224, 238, 224)', + 'honeydew3': 'rgb(193, 205, 193)', + 'honeydew4': 'rgb(131, 139, 131)', + 'hotpink': 'rgb(255, 105, 180)', + 'hotpink1': 'rgb(255, 110, 180)', + 'hotpink2': 'rgb(238, 106, 167)', + 'hotpink3': 'rgb(205, 96, 144)', + 'hotpink4': 'rgb(139, 58, 98)', + 'indianred': 'rgb(205, 92, 92)', + 'indianred1': 'rgb(255, 106, 106)', + 'indianred2': 'rgb(238, 99, 99)', + 'indianred3': 'rgb(205, 85, 85)', + 'indianred4': 'rgb(139, 58, 58)', + 'ivory': 'rgb(255, 255, 240)', + 'ivory1': 'rgb(255, 255, 240)', + 'ivory2': 'rgb(238, 238, 224)', + 'ivory3': 'rgb(205, 205, 193)', + 'ivory4': 'rgb(139, 139, 131)', + 'khaki': 'rgb(240, 230, 140)', + 'khaki1': 'rgb(255, 246, 143)', + 'khaki2': 'rgb(238, 230, 133)', + 'khaki3': 'rgb(205, 198, 115)', + 'khaki4': 'rgb(139, 134, 78)', + 'lavender': 'rgb(230, 230, 250)', + 'lavenderblush': 'rgb(255, 240, 245)', + 'lavenderblush1': 'rgb(255, 240, 245)', + 'lavenderblush2': 'rgb(238, 224, 229)', + 'lavenderblush3': 'rgb(205, 193, 197)', + 'lavenderblush4': 'rgb(139, 131, 134)', + 'lawngreen': 'rgb(124, 252, 0)', + 'lemonchiffon': 'rgb(255, 250, 205)', + 'lemonchiffon1': 'rgb(255, 250, 205)', + 'lemonchiffon2': 'rgb(238, 233, 191)', + 'lemonchiffon3': 'rgb(205, 201, 165)', + 'lemonchiffon4': 'rgb(139, 137, 112)', + 'lightblue': 'rgb(173, 216, 230)', + 'lightblue1': 'rgb(191, 239, 255)', + 'lightblue2': 'rgb(178, 223, 238)', + 'lightblue3': 'rgb(154, 192, 205)', + 'lightblue4': 'rgb(104, 131, 139)', + 'lightcoral': 'rgb(240, 128, 128)', + 'lightcyan': 'rgb(224, 255, 255)', + 'lightcyan1': 'rgb(224, 255, 255)', + 'lightcyan2': 'rgb(209, 238, 238)', + 'lightcyan3': 'rgb(180, 205, 205)', + 'lightcyan4': 'rgb(122, 139, 139)', + 'lightgoldenrod': 'rgb(238, 221, 130)', + 'lightgoldenrod1': 'rgb(255, 236, 139)', + 'lightgoldenrod2': 'rgb(238, 220, 130)', + 'lightgoldenrod3': 'rgb(205, 190, 112)', + 'lightgoldenrod4': 'rgb(139, 129, 76)', + 'lightgoldenrodyellow': 'rgb(250, 250, 210)', + 'lightgray': 'rgb(211, 211, 211)', + 'lightgreen': 'rgb(144, 238, 144)', + 'lightgrey': 'rgb(211, 211, 211)', + 'lightpink': 'rgb(255, 182, 193)', + 'lightpink1': 'rgb(255, 174, 185)', + 'lightpink2': 'rgb(238, 162, 173)', + 'lightpink3': 'rgb(205, 140, 149)', + 'lightpink4': 'rgb(139, 95, 101)', + 'lightsalmon': 'rgb(255, 160, 122)', + 'lightsalmon1': 'rgb(255, 160, 122)', + 'lightsalmon2': 'rgb(238, 149, 114)', + 'lightsalmon3': 'rgb(205, 129, 98)', + 'lightsalmon4': 'rgb(139, 87, 66)', + 'lightseagreen': 'rgb(32, 178, 170)', + 'lightskyblue': 'rgb(135, 206, 250)', + 'lightskyblue1': 'rgb(176, 226, 255)', + 'lightskyblue2': 'rgb(164, 211, 238)', + 'lightskyblue3': 'rgb(141, 182, 205)', + 'lightskyblue4': 'rgb(96, 123, 139)', + 'lightslateblue': 'rgb(132, 112, 255)', + 'lightslategray': 'rgb(119, 136, 153)', + 'lightslategrey': 'rgb(119, 136, 153)', + 'lightsteelblue': 'rgb(176, 196, 222)', + 'lightsteelblue1': 'rgb(202, 225, 255)', + 'lightsteelblue2': 'rgb(188, 210, 238)', + 'lightsteelblue3': 'rgb(162, 181, 205)', + 'lightsteelblue4': 'rgb(110, 123, 139)', + 'lightyellow': 'rgb(255, 255, 224)', + 'lightyellow1': 'rgb(255, 255, 224)', + 'lightyellow2': 'rgb(238, 238, 209)', + 'lightyellow3': 'rgb(205, 205, 180)', + 'lightyellow4': 'rgb(139, 139, 122)', + 'limegreen': 'rgb(50, 205, 50)', + 'linen': 'rgb(250, 240, 230)', + 'magenta': 'rgb(255, 0, 255)', + 'magenta1': 'rgb(255, 0, 255)', + 'magenta2': 'rgb(238, 0, 238)', + 'magenta3': 'rgb(205, 0, 205)', + 'magenta4': 'rgb(139, 0, 139)', + 'maroon': 'rgb(176, 48, 96)', + 'maroon1': 'rgb(255, 52, 179)', + 'maroon2': 'rgb(238, 48, 167)', + 'maroon3': 'rgb(205, 41, 144)', + 'maroon4': 'rgb(139, 28, 98)', + 'mediumaquamarine': 'rgb(102, 205, 170)', + 'mediumblue': 'rgb(0, 0, 205)', + 'mediumorchid': 'rgb(186, 85, 211)', + 'mediumorchid1': 'rgb(224, 102, 255)', + 'mediumorchid2': 'rgb(209, 95, 238)', + 'mediumorchid3': 'rgb(180, 82, 205)', + 'mediumorchid4': 'rgb(122, 55, 139)', + 'mediumpurple': 'rgb(147, 112, 219)', + 'mediumpurple1': 'rgb(171, 130, 255)', + 'mediumpurple2': 'rgb(159, 121, 238)', + 'mediumpurple3': 'rgb(137, 104, 205)', + 'mediumpurple4': 'rgb(93, 71, 139)', + 'mediumseagreen': 'rgb(60, 179, 113)', + 'mediumslateblue': 'rgb(123, 104, 238)', + 'mediumspringgreen': 'rgb(0, 250, 154)', + 'mediumturquoise': 'rgb(72, 209, 204)', + 'mediumvioletred': 'rgb(199, 21, 133)', + 'midnightblue': 'rgb(25, 25, 112)', + 'mintcream': 'rgb(245, 255, 250)', + 'mistyrose': 'rgb(255, 228, 225)', + 'mistyrose1': 'rgb(255, 228, 225)', + 'mistyrose2': 'rgb(238, 213, 210)', + 'mistyrose3': 'rgb(205, 183, 181)', + 'mistyrose4': 'rgb(139, 125, 123)', + 'moccasin': 'rgb(255, 228, 181)', + 'navajowhite': 'rgb(255, 222, 173)', + 'navajowhite1': 'rgb(255, 222, 173)', + 'navajowhite2': 'rgb(238, 207, 161)', + 'navajowhite3': 'rgb(205, 179, 139)', + 'navajowhite4': 'rgb(139, 121, 94)', + 'navy': 'rgb(0, 0, 128)', + 'navyblue': 'rgb(0, 0, 128)', + 'oldlace': 'rgb(253, 245, 230)', + 'olivedrab': 'rgb(107, 142, 35)', + 'olivedrab1': 'rgb(192, 255, 62)', + 'olivedrab2': 'rgb(179, 238, 58)', + 'olivedrab3': 'rgb(154, 205, 50)', + 'olivedrab4': 'rgb(105, 139, 34)', + 'orange': 'rgb(255, 165, 0)', + 'orange1': 'rgb(255, 165, 0)', + 'orange2': 'rgb(238, 154, 0)', + 'orange3': 'rgb(205, 133, 0)', + 'orange4': 'rgb(139, 90, 0)', + 'orangered': 'rgb(255, 69, 0)', + 'orangered1': 'rgb(255, 69, 0)', + 'orangered2': 'rgb(238, 64, 0)', + 'orangered3': 'rgb(205, 55, 0)', + 'orangered4': 'rgb(139, 37, 0)', + 'orchid': 'rgb(218, 112, 214)', + 'orchid1': 'rgb(255, 131, 250)', + 'orchid2': 'rgb(238, 122, 233)', + 'orchid3': 'rgb(205, 105, 201)', + 'orchid4': 'rgb(139, 71, 137)', + 'palegoldenrod': 'rgb(238, 232, 170)', + 'palegreen': 'rgb(152, 251, 152)', + 'palegreen1': 'rgb(154, 255, 154)', + 'palegreen2': 'rgb(144, 238, 144)', + 'palegreen3': 'rgb(124, 205, 124)', + 'palegreen4': 'rgb(84, 139, 84)', + 'paleturquoise': 'rgb(175, 238, 238)', + 'paleturquoise1': 'rgb(187, 255, 255)', + 'paleturquoise2': 'rgb(174, 238, 238)', + 'paleturquoise3': 'rgb(150, 205, 205)', + 'paleturquoise4': 'rgb(102, 139, 139)', + 'palevioletred': 'rgb(219, 112, 147)', + 'palevioletred1': 'rgb(255, 130, 171)', + 'palevioletred2': 'rgb(238, 121, 159)', + 'palevioletred3': 'rgb(205, 104, 137)', + 'palevioletred4': 'rgb(139, 71, 93)', + 'papayawhip': 'rgb(255, 239, 213)', + 'peachpuff': 'rgb(255, 218, 185)', + 'peachpuff1': 'rgb(255, 218, 185)', + 'peachpuff2': 'rgb(238, 203, 173)', + 'peachpuff3': 'rgb(205, 175, 149)', + 'peachpuff4': 'rgb(139, 119, 101)', + 'peru': 'rgb(205, 133, 63)', + 'pink': 'rgb(255, 192, 203)', + 'pink1': 'rgb(255, 181, 197)', + 'pink2': 'rgb(238, 169, 184)', + 'pink3': 'rgb(205, 145, 158)', + 'pink4': 'rgb(139, 99, 108)', + 'plum': 'rgb(221, 160, 221)', + 'plum1': 'rgb(255, 187, 255)', + 'plum2': 'rgb(238, 174, 238)', + 'plum3': 'rgb(205, 150, 205)', + 'plum4': 'rgb(139, 102, 139)', + 'powderblue': 'rgb(176, 224, 230)', + 'purple': 'rgb(160, 32, 240)', + 'purple1': 'rgb(155, 48, 255)', + 'purple2': 'rgb(145, 44, 238)', + 'purple3': 'rgb(125, 38, 205)', + 'purple4': 'rgb(85, 26, 139)', + 'red': 'rgb(255, 0, 0)', + 'red1': 'rgb(255, 0, 0)', + 'red2': 'rgb(238, 0, 0)', + 'red3': 'rgb(205, 0, 0)', + 'red4': 'rgb(139, 0, 0)', + 'rosybrown': 'rgb(188, 143, 143)', + 'rosybrown1': 'rgb(255, 193, 193)', + 'rosybrown2': 'rgb(238, 180, 180)', + 'rosybrown3': 'rgb(205, 155, 155)', + 'rosybrown4': 'rgb(139, 105, 105)', + 'royalblue': 'rgb(65, 105, 225)', + 'royalblue1': 'rgb(72, 118, 255)', + 'royalblue2': 'rgb(67, 110, 238)', + 'royalblue3': 'rgb(58, 95, 205)', + 'royalblue4': 'rgb(39, 64, 139)', + 'saddlebrown': 'rgb(139, 69, 19)', + 'salmon': 'rgb(250, 128, 114)', + 'salmon1': 'rgb(255, 140, 105)', + 'salmon2': 'rgb(238, 130, 98)', + 'salmon3': 'rgb(205, 112, 84)', + 'salmon4': 'rgb(139, 76, 57)', + 'sandybrown': 'rgb(244, 164, 96)', + 'seagreen': 'rgb(46, 139, 87)', + 'seagreen1': 'rgb(84, 255, 159)', + 'seagreen2': 'rgb(78, 238, 148)', + 'seagreen3': 'rgb(67, 205, 128)', + 'seagreen4': 'rgb(46, 139, 87)', + 'seashell': 'rgb(255, 245, 238)', + 'seashell1': 'rgb(255, 245, 238)', + 'seashell2': 'rgb(238, 229, 222)', + 'seashell3': 'rgb(205, 197, 191)', + 'seashell4': 'rgb(139, 134, 130)', + 'sienna': 'rgb(160, 82, 45)', + 'sienna1': 'rgb(255, 130, 71)', + 'sienna2': 'rgb(238, 121, 66)', + 'sienna3': 'rgb(205, 104, 57)', + 'sienna4': 'rgb(139, 71, 38)', + 'skyblue': 'rgb(135, 206, 235)', + 'skyblue1': 'rgb(135, 206, 255)', + 'skyblue2': 'rgb(126, 192, 238)', + 'skyblue3': 'rgb(108, 166, 205)', + 'skyblue4': 'rgb(74, 112, 139)', + 'slateblue': 'rgb(106, 90, 205)', + 'slateblue1': 'rgb(131, 111, 255)', + 'slateblue2': 'rgb(122, 103, 238)', + 'slateblue3': 'rgb(105, 89, 205)', + 'slateblue4': 'rgb(71, 60, 139)', + 'slategray': 'rgb(112, 128, 144)', + 'slategray1': 'rgb(198, 226, 255)', + 'slategray2': 'rgb(185, 211, 238)', + 'slategray3': 'rgb(159, 182, 205)', + 'slategray4': 'rgb(108, 123, 139)', + 'slategrey': 'rgb(112, 128, 144)', + 'snow': 'rgb(255, 250, 250)', + 'snow1': 'rgb(255, 250, 250)', + 'snow2': 'rgb(238, 233, 233)', + 'snow3': 'rgb(205, 201, 201)', + 'snow4': 'rgb(139, 137, 137)', + 'springgreen': 'rgb(0, 255, 127)', + 'springgreen1': 'rgb(0, 255, 127)', + 'springgreen2': 'rgb(0, 238, 118)', + 'springgreen3': 'rgb(0, 205, 102)', + 'springgreen4': 'rgb(0, 139, 69)', + 'steelblue': 'rgb(70, 130, 180)', + 'steelblue1': 'rgb(99, 184, 255)', + 'steelblue2': 'rgb(92, 172, 238)', + 'steelblue3': 'rgb(79, 148, 205)', + 'steelblue4': 'rgb(54, 100, 139)', + 'tan': 'rgb(210, 180, 140)', + 'tan1': 'rgb(255, 165, 79)', + 'tan2': 'rgb(238, 154, 73)', + 'tan3': 'rgb(205, 133, 63)', + 'tan4': 'rgb(139, 90, 43)', + 'thistle': 'rgb(216, 191, 216)', + 'thistle1': 'rgb(255, 225, 255)', + 'thistle2': 'rgb(238, 210, 238)', + 'thistle3': 'rgb(205, 181, 205)', + 'thistle4': 'rgb(139, 123, 139)', + 'tomato': 'rgb(255, 99, 71)', + 'tomato1': 'rgb(255, 99, 71)', + 'tomato2': 'rgb(238, 92, 66)', + 'tomato3': 'rgb(205, 79, 57)', + 'tomato4': 'rgb(139, 54, 38)', + 'turquoise': 'rgb(64, 224, 208)', + 'turquoise1': 'rgb(0, 245, 255)', + 'turquoise2': 'rgb(0, 229, 238)', + 'turquoise3': 'rgb(0, 197, 205)', + 'turquoise4': 'rgb(0, 134, 139)', + 'violet': 'rgb(238, 130, 238)', + 'violetred': 'rgb(208, 32, 144)', + 'violetred1': 'rgb(255, 62, 150)', + 'violetred2': 'rgb(238, 58, 140)', + 'violetred3': 'rgb(205, 50, 120)', + 'violetred4': 'rgb(139, 34, 82)', + 'wheat': 'rgb(245, 222, 179)', + 'wheat1': 'rgb(255, 231, 186)', + 'wheat2': 'rgb(238, 216, 174)', + 'wheat3': 'rgb(205, 186, 150)', + 'wheat4': 'rgb(139, 126, 102)', + 'white': 'rgb(255, 255, 255)', + 'whitesmoke': 'rgb(245, 245, 245)', + 'yellow': 'rgb(255, 255, 0)', + 'yellow1': 'rgb(255, 255, 0)', + 'yellow2': 'rgb(238, 238, 0)', + 'yellow3': 'rgb(205, 205, 0)', + 'yellow4': 'rgb(139, 139, 0)', + 'yellowgreen': 'rgb(154, 205, 50)', }; // SOURCE FILE: libdot/js/lib_f.js // Copyright (c) 2012 The Chromium OS Authors. All rights reserved. @@ -1428,18 +1752,23 @@ lib.f = {}; * name: "Google+" }); * * Will result in "Hello, Google%2B". + * + * @param {string} str String containing variable references. + * @param {!Object} vars Variables to substitute in. + * @return {string} String with references substituted. */ lib.f.replaceVars = function(str, vars) { - return str.replace(/%([a-z]*)\(([^\)]+)\)/gi, function(match, fn, varname) { - if (typeof vars[varname] == 'undefined') - throw 'Unknown variable: ' + varname; + return str.replace(/%([a-z]*)\(([^)]+)\)/gi, function(match, fn, varname) { + if (typeof vars[varname] == 'undefined') { + throw new Error(`Unknown variable: ${varname}`); + } - var rv = vars[varname]; + let rv = vars[varname]; if (fn in lib.f.replaceVars.functions) { rv = lib.f.replaceVars.functions[fn](rv); } else if (fn) { - throw 'Unknown escape function: ' + fn; + throw new Error(`Unknown escape function: ${fn}`); } return rv; @@ -1455,49 +1784,64 @@ lib.f.replaceVars.functions = { encodeURI: encodeURI, encodeURIComponent: encodeURIComponent, escapeHTML: function(str) { - var map = { + const map = { '<': '<', '>': '>', '&': '&', '"': '"', - "'": ''' + "'": ''', }; - return str.replace(/[<>&\"\']/g, (m) => map[m]); - } + return str.replace(/[<>&"']/g, (m) => map[m]); + }, }; +/** + * Convert a relative path to a fully qulified URI. + * + * @param {string} path Relative path + * @return {string} Fully qualified URI. + */ lib.f.getURL = function(path) { - if (lib.f.getURL.chromeSupported()) + if (lib.f.getURL.chromeSupported()) { return chrome.runtime.getURL(path); + } return path; }; +/** + * Determine whether the runtime is Chrome (or equiv). + * + * @return {boolean} True if chrome.runtime.getURL is supported. + */ lib.f.getURL.chromeSupported = function() { - return window.chrome && chrome.runtime && chrome.runtime.getURL; + return !!(window.chrome && chrome.runtime && chrome.runtime.getURL); }; /** * Clamp a given integer to a specified range. * - * @param {integer} v The value to be clamped. - * @param {integer} min The minimum acceptable value. - * @param {integer} max The maximum acceptable value. + * @param {number} v The value to be clamped. + * @param {number} min The minimum acceptable value. + * @param {number} max The maximum acceptable value. + * @return {number} The clamped value. */ lib.f.clamp = function(v, min, max) { - if (v < min) + if (v < min) { return min; - if (v > max) + } + if (v > max) { return max; + } return v; }; /** * Left pad a number to a given length with leading zeros. * - * @param {string|integer} number The number to pad. - * @param {integer} length The desired length. + * @param {string|number} number The number to pad. + * @param {number} length The desired length. * @return {string} The padded number as a string. */ lib.f.zpad = function(number, length) { @@ -1505,32 +1849,7 @@ lib.f.zpad = function(number, length) { }; /** - * Return a string containing a given number of space characters. - * - * This method maintains a static cache of the largest amount of whitespace - * ever requested. It shouldn't be used to generate an insanely huge amount of - * whitespace. - * - * @param {integer} length The desired amount of whitespace. - * @param {string} A string of spaces of the requested length. - */ -lib.f.getWhitespace = function(length) { - if (length <= 0) - return ''; - - var f = this.getWhitespace; - if (!f.whitespace) - f.whitespace = ' '; - - while (length > f.whitespace.length) { - f.whitespace += f.whitespace; - } - - return f.whitespace.substr(0, length); -}; - -/** - * Return the current call stack after skipping a given number of frames. + * Return the current call stack after skipping a given number of frames. * * This method is intended to be used for debugging only. It returns an * Object instead of an Array, because the console stringifies arrays by @@ -1547,6 +1866,7 @@ lib.f.getWhitespace = function(length) { * @param {number=} ignoreFrames How many inner stack frames to ignore. The * innermost 'getStack' call is always ignored. * @param {number=} count How many frames to return. + * @return {!Array} The stack frames. */ lib.f.getStack = function(ignoreFrames = 0, count = undefined) { const stackArray = (new Error()).stack.split('\n'); @@ -1557,16 +1877,18 @@ lib.f.getStack = function(ignoreFrames = 0, count = undefined) { ignoreFrames += 2; const max = stackArray.length - ignoreFrames; - if (count === undefined) + if (count === undefined) { count = max; - else + } else { count = lib.f.clamp(count, 0, max); + } // Remove the leading spaces and "at" from each line: // ' at window.onload (file:///.../lib_test.js:11:18)' const stackObject = new Array(); - for (let i = ignoreFrames; i < count + ignoreFrames; ++i) + for (let i = ignoreFrames; i < count + ignoreFrames; ++i) { stackObject.push(stackArray[i].replace(/^\s*at\s+/, '')); + } return stackObject; }; @@ -1582,9 +1904,9 @@ lib.f.getStack = function(ignoreFrames = 0, count = undefined) { * @param {number} denominator * @return {number} */ -lib.f.smartFloorDivide = function(numerator, denominator) { - var val = numerator / denominator; - var ceiling = Math.ceil(val); +lib.f.smartFloorDivide = function(numerator, denominator) { + const val = numerator / denominator; + const ceiling = Math.ceil(val); if (ceiling - val < .0001) { return ceiling; } else { @@ -1606,13 +1928,14 @@ lib.f.randomInt = function(min, max) { /** * Get the current OS. * - * @return {Promise} A promise that resolves to a constant in + * @return {!Promise} A promise that resolves to a constant in * runtime.PlatformOs. */ lib.f.getOs = function() { // Try the brower extensions API. - if (window.browser && browser.runtime && browser.runtime.getPlatformInfo) + if (window.browser && browser.runtime && browser.runtime.getPlatformInfo) { return browser.runtime.getPlatformInfo().then((info) => info.os); + } // Use the native Chrome API if available. if (window.chrome && chrome.runtime && chrome.runtime.getPlatformInfo) { @@ -1624,16 +1947,17 @@ lib.f.getOs = function() { // browser API above. if (window.navigator && navigator.userAgent) { const ua = navigator.userAgent; - if (ua.includes('Mac OS X')) + if (ua.includes('Mac OS X')) { return Promise.resolve('mac'); - else if (ua.includes('CrOS')) + } else if (ua.includes('CrOS')) { return Promise.resolve('cros'); - else if (ua.includes('Linux')) + } else if (ua.includes('Linux')) { return Promise.resolve('linux'); - else if (ua.includes('Android')) + } else if (ua.includes('Android')) { return Promise.resolve('android'); - else if (ua.includes('Windows')) + } else if (ua.includes('Windows')) { return Promise.resolve('windows'); + } } // Probe node environment. @@ -1653,8 +1977,9 @@ lib.f.getOs = function() { lib.f.getChromeMilestone = function() { if (window.navigator && navigator.userAgent) { const ary = navigator.userAgent.match(/\sChrome\/(\d+)/); - if (ary) - return parseInt(ary[1]); + if (ary) { + return parseInt(ary[1], 10); + } } // Returning NaN will make all number comparisons fail. @@ -1667,20 +1992,22 @@ lib.f.getChromeMilestone = function() { * This object might live in different locations, and it isn't always defined * (if there hasn't been a "last error"). Wrap all that ugliness here. * - * @param {string=} defaultMsg The default message if no error is found. - * @return {string} The last error message from the browser. + * @param {?string=} defaultMsg The default message if no error is found. + * @return {?string} The last error message from the browser. */ lib.f.lastError = function(defaultMsg = null) { let lastError; - if (window.browser && browser.runtime) + if (window.browser && browser.runtime) { lastError = browser.runtime.lastError; - else if (window.chrome && chrome.runtime) + } else if (window.chrome && chrome.runtime) { lastError = chrome.runtime.lastError; + } - if (lastError && lastError.message) + if (lastError && lastError.message) { return lastError.message; - else + } else { return defaultMsg; + } }; /** @@ -1693,9 +2020,9 @@ lib.f.lastError = function(defaultMsg = null) { * @param {string=} url The URL to point the new window to. * @param {string=} name The name of the new window. * @param {string=} features The window features to enable. - * @return {Window} The newly opened window. + * @return {?Window} The newly opened window. */ -lib.f.openWindow = function(url, name=undefined, features=undefined) { +lib.f.openWindow = function(url, name = undefined, features = undefined) { // We create the window first without the URL loaded. const win = window.open(undefined, name, features); @@ -1733,21 +2060,31 @@ lib.i18n.browser_ = window.chrome && chrome.i18n ? chrome.i18n : null; +/** + * Return whether the browser supports i18n natively. + * + * @return {boolean} True if browser.i18n or chrome.i18n exists. + */ +lib.i18n.browserSupported = function() { + return lib.i18n.browser_ !== null; +}; + /** * Get the list of accepted UI languages. * * https://developer.mozilla.org/en-US/Add-ons/WebExtensions/API/i18n/getAcceptLanguages * - * @param {function(Array)} callback Function to invoke with the results. The - * parameter is a list of locale names. + * @return {!Promise>} Promise resolving to the list of locale + * names. */ -lib.i18n.getAcceptLanguages = function(callback) { +lib.i18n.getAcceptLanguages = function() { if (lib.i18n.browser_) { - lib.i18n.browser_.getAcceptLanguages(callback); + return new Promise((resolve) => { + lib.i18n.browser_.getAcceptLanguages(resolve); + }); } else { - setTimeout(function() { - callback([navigator.language.replace(/-/g, '_')]); - }, 0); + const languages = navigator.languages || [navigator.language]; + return Promise.resolve(languages); } }; @@ -1757,7 +2094,7 @@ lib.i18n.getAcceptLanguages = function(callback) { * https://developer.mozilla.org/en-US/Add-ons/WebExtensions/API/i18n/getMessage * * @param {string} msgname The id for this localized string. - * @param {string[]=} substitutions Any replacements in the string. + * @param {?Array=} substitutions Any replacements in the string. * @param {string=} fallback Translation if the message wasn't found. * @return {string} The translated message. */ @@ -1765,8 +2102,9 @@ lib.i18n.getMessage = function(msgname, substitutions = [], fallback = '') { // First let the native browser APIs handle everything for us. if (lib.i18n.browser_) { const message = lib.i18n.browser_.getMessage(msgname, substitutions); - if (message) + if (message) { return message; + } } // Do our best to get something reasonable. @@ -1780,7 +2118,8 @@ lib.i18n.getMessage = function(msgname, substitutions = [], fallback = '') { * always replaced/removed regardless of the specified substitutions. * * @param {string} msg String containing the message and argument references. - * @param {string[]=} args Array containing the argument values. + * @param {(?Array|string)=} args Array containing the argument values, + * or single value. * @return {string} The message with replacements expanded. */ lib.i18n.replaceReferences = function(msg, args = []) { @@ -1796,6 +2135,61 @@ lib.i18n.replaceReferences = function(msg, args = []) { return index <= args.length ? args[index - 1] : ''; }); }; + +/** + * This function aims to copy the chrome.i18n mapping from language to which + * _locales//messages.json translation is used. E.g. en-AU maps to + * en_GB. + * https://cs.chromium.org/chromium/src/ui/base/l10n/l10n_util.cc?type=cs&q=CheckAndResolveLocale + * + * @param {string} language language from navigator.languages. + * @return {!Array} priority list of locales for translation. + */ +lib.i18n.resolveLanguage = function(language) { + const [lang, region] = language.toLowerCase().split(/[-_]/, 2); + + // Map es-RR other than es-ES to es-419 (Chrome's Latin American + // Spanish locale). + if (lang == 'es') { + if (region != undefined && region != 'es') { + return ['es_419']; + } + return ['es']; + } + + // Map pt-RR other than pt-BR to pt-PT. Note that "pt" by itself maps to + // pt-BR (logic below). + if (lang == 'pt') { + if (region == 'br' || region == undefined) { + return ['pt_BR']; + } + return ['pt_PT']; + } + + // Map zh-HK and zh-MO to zh-TW. Otherwise, zh-FOO is mapped to zh-CN. + if (lang == 'zh') { + if (region == 'tw' || region == 'hk' || region == 'mo') { + return ['zh_TW']; + } + return ['zh_CN']; + } + + // Map Australian, Canadian, Indian, New Zealand and South African + // English to British English for now. + if (lang == 'en') { + if (region == 'au' || region == 'ca' || region == 'in' || region == 'nz' || + region == 'za') { + return ['en_GB']; + } + return ['en']; + } + + if (region) { + return [language.replace(/-/g, '_'), lang]; + } else { + return [lang]; + } +}; // SOURCE FILE: libdot/js/lib_message_manager.js // Copyright (c) 2012 The Chromium OS Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be @@ -1811,141 +2205,177 @@ lib.i18n.replaceReferences = function(msg, args = []) { * 3. The client code may be part of a library packaged in a third-party * Chrome extension. * - * @param {Array} languages List of languages to load, in the order they - * should be loaded. Newer messages replace older ones. 'en' is - * automatically added as the first language if it is not already present. + * @param {!Array} languages List of languages to load, in the order + * they are preferred. The first language found will be used. 'en' is + * automatically added as the last language if it is not already present. + * @param {boolean=} useCrlf If true, '\n' in messages are substituted for + * '\r\n'. This fixes the translation process which discards '\r' + * characters. + * @constructor */ -lib.MessageManager = function(languages) { - this.languages_ = languages.map((el) => el.replace(/-/g, '_')); +lib.MessageManager = function(languages, useCrlf = false) { + /** + * @private {!Array} + * @const + */ + this.languages_ = []; + let stop = false; + for (let i = 0; i < languages.length && !stop; i++) { + for (const lang of lib.i18n.resolveLanguage(languages[i])) { + // There is no point having any language with lower priorty than 'en' + // since 'en' always contains all messages. + if (lang == 'en') { + stop = true; + break; + } + if (!this.languages_.includes(lang)) { + this.languages_.push(lang); + } + } + } + // Always have 'en' as last fallback. + this.languages_.push('en'); - if (this.languages_.indexOf('en') == -1) - this.languages_.unshift('en'); + this.useCrlf = useCrlf; - this.messages = {}; + /** + * @private {!Object} + * @const + */ + this.messages_ = {}; }; +/** + * @typedef {!Object} + */ +lib.MessageManager.Messages; + /** * Add message definitions to the message manager. * * This takes an object of the same format of a Chrome messages.json file. See * . + * + * @param {!lib.MessageManager.Messages} defs The message to add to the + * database. */ lib.MessageManager.prototype.addMessages = function(defs) { - for (var key in defs) { - var def = defs[key]; + for (const key in defs) { + const def = defs[key]; if (!def.placeholders) { - this.messages[key] = def.message; + // Upper case key into this.messages_ since our translated + // bundles are lower case, but we request msg as upper. + this.messages_[key.toUpperCase()] = def.message; } else { // Replace "$NAME$" placeholders with "$1", etc. - this.messages[key] = def.message.replace( - /\$([a-z][^\s\$]+)\$/ig, - function(m, name) { - return defs[key].placeholders[name.toLowerCase()].content; + this.messages_[key.toUpperCase()] = + def.message.replace(/\$([a-z][^\s$]+)\$/ig, function(m, name) { + return defs[key].placeholders[name.toUpperCase()].content; }); } } }; /** - * Load the first available language message bundle. + * Load language message bundles. Loads in reverse order so that higher + * priority languages overwrite lower priority. * * @param {string} pattern A url pattern containing a "$1" where the locale * name should go. - * @param {function(Array,Array)} onComplete Function to be called when loading - * is complete. The two arrays are the list of successful and failed - * locale names. If the first parameter is length 0, no locales were - * loaded. - */ -lib.MessageManager.prototype.findAndLoadMessages = function( - pattern, onComplete) { - var languages = this.languages_.concat(); - var loaded = []; - var failed = []; - - function onLanguageComplete(state) { - if (state) { - loaded = languages.shift(); - } else { - failed = languages.shift(); - } + */ +lib.MessageManager.prototype.findAndLoadMessages = async function(pattern) { + if (lib.i18n.browserSupported()) { + return; + } - if (languages.length) { - tryNextLanguage(); - } else { - onComplete(loaded, failed); + for (let i = this.languages_.length - 1; i >= 0; i--) { + const lang = this.languages_[i]; + const url = lib.i18n.replaceReferences(pattern, lang); + try { + await this.loadMessages(url); + } catch (e) { + console.warn( + `Error fetching ${lang} messages at ${url}`, e, + 'Trying all languages in reverse order:', this.languages_); } } - - var tryNextLanguage = function() { - this.loadMessages(this.replaceReferences(pattern, languages), - onLanguageComplete.bind(this, true), - onLanguageComplete.bind(this, false)); - }.bind(this); - - tryNextLanguage(); }; /** * Load messages from a messages.json file. + * + * @param {string} url The URL to load the messages from. + * @return {!Promise} */ -lib.MessageManager.prototype.loadMessages = function( - url, onSuccess, opt_onError) { - var xhr = new XMLHttpRequest(); - - xhr.onload = () => { - this.addMessages(JSON.parse(xhr.responseText)); - onSuccess(); - }; - if (opt_onError) - xhr.onerror = () => opt_onError(xhr); +lib.MessageManager.prototype.loadMessages = function(url) { + return new Promise((resolve, reject) => { + const xhr = new XMLHttpRequest(); + xhr.onload = () => { + try { + this.addMessages(/** @type {!lib.MessageManager.Messages} */ ( + JSON.parse(xhr.responseText))); + resolve(); + } catch (e) { + // Error parsing JSON. + reject(e); + } + }; + xhr.onerror = () => reject(xhr); - xhr.open('GET', url); - xhr.send(); + xhr.open('GET', url); + xhr.send(); + }); }; -/** - * Per-instance copy of replaceReferences. - */ -lib.MessageManager.prototype.replaceReferences = lib.i18n.replaceReferences; - /** * Get a message by name, optionally replacing arguments too. * * @param {string} msgname String containing the name of the message to get. - * @param {Array} opt_args Optional array containing the argument values. - * @param {string} opt_default Optional value to return if the msgname is not + * @param {!Array=} args Optional array containing the argument values. + * @param {string=} fallback Optional value to return if the msgname is not * found. Returns the message name by default. + * @return {string} The formatted translation. */ -lib.MessageManager.prototype.get = function(msgname, opt_args, opt_default) { +lib.MessageManager.prototype.get = function(msgname, args, fallback) { // First try the integrated browser getMessage. We prefer that over any // registered messages as only the browser supports translations. - let message = lib.i18n.getMessage(msgname, opt_args); - if (message) - return message; - - // Look it up in the registered cache next. - message = this.messages[msgname]; + let message = lib.i18n.getMessage(msgname, args); if (!message) { - console.warn('Unknown message: ' + msgname); - message = opt_default === undefined ? msgname : opt_default; - // Register the message with the default to avoid multiple warnings. - this.messages[msgname] = message; + // Look it up in the registered cache next. + message = this.messages_[msgname]; + if (!message) { + console.warn('Unknown message: ' + msgname); + message = fallback === undefined ? msgname : fallback; + // Register the message with the default to avoid multiple warnings. + this.messages_[msgname] = message; + } + message = lib.i18n.replaceReferences(message, args); } - - return this.replaceReferences(message, opt_args); + if (this.useCrlf) { + message = message.replace(/\n/g, '\r\n'); + } + return message; }; /** - * Process all of the "i18n" html attributes found in a given dom fragment. + * Process all of the "i18n" html attributes found in a given element. * * The real work happens in processI18nAttribute. + * + * @param {!HTMLDocument|!Element} node The element whose nodes will be + * translated. */ -lib.MessageManager.prototype.processI18nAttributes = function(dom) { - var nodes = dom.querySelectorAll('[i18n]'); +lib.MessageManager.prototype.processI18nAttributes = function(node) { + const nodes = node.querySelectorAll('[i18n]'); - for (var i = 0; i < nodes.length; i++) + for (let i = 0; i < nodes.length; i++) { this.processI18nAttribute(nodes[i]); + } }; /** @@ -1967,15 +2397,18 @@ lib.MessageManager.prototype.processI18nAttributes = function(dom) { * The aria-label message name will be computed as "SEND_BUTTON_ARIA_LABEL". * Notice that the "id" attribute was appended to the target attribute, and * the result converted to UPPER_AND_UNDER style. + * + * @param {!Element} node The element to translate. */ lib.MessageManager.prototype.processI18nAttribute = function(node) { // Convert the "lower-and-dashes" attribute names into // "UPPER_AND_UNDER" style. const thunk = (str) => str.replace(/-/g, '_').toUpperCase(); - var i18n = node.getAttribute('i18n'); - if (!i18n) + let i18n = node.getAttribute('i18n'); + if (!i18n) { return; + } try { i18n = JSON.parse(i18n); @@ -1985,11 +2418,11 @@ lib.MessageManager.prototype.processI18nAttribute = function(node) { } // Load all the messages specified in the i18n attributes. - for (var key in i18n) { + for (let key in i18n) { // The node attribute we'll be setting. - var attr = key; + const attr = key; - var msgname = i18n[key]; + let msgname = i18n[key]; // For "=foo", re-use the referenced message name. if (msgname.startsWith('=')) { key = msgname.substr(1); @@ -1997,15 +2430,17 @@ lib.MessageManager.prototype.processI18nAttribute = function(node) { } // For "$foo", calculate the message name. - if (msgname.startsWith('$')) + if (msgname.startsWith('$')) { msgname = thunk(node.getAttribute(msgname.substr(1)) + '_' + key); + } // Finally load the message. - var msg = this.get(msgname); - if (attr == '_') + const msg = this.get(msgname); + if (attr == '_') { node.textContent = msg; - else + } else { node.setAttribute(attr, msg); + } } }; // SOURCE FILE: libdot/js/lib_preference_manager.js @@ -2024,14 +2459,15 @@ lib.MessageManager.prototype.processI18nAttribute = function(node) { * may be a chrome api to store sync-able name/value pairs, and we'd want * that. * - * @param {lib.Storage.*} storage The storage object to use as a backing + * @param {!lib.Storage} storage The storage object to use as a backing * store. - * @param {string} opt_prefix The optional prefix to be used for all preference + * @param {string=} prefix The optional prefix to be used for all preference * names. The '/' character should be used to separate levels of hierarchy, * if you're going to have that kind of thing. If provided, the prefix * should start with a '/'. If not provided, it defaults to '/'. + * @constructor */ -lib.PreferenceManager = function(storage, opt_prefix) { +lib.PreferenceManager = function(storage, prefix = '/') { this.storage = storage; this.storageObserver_ = this.onStorageChange_.bind(this); @@ -2040,9 +2476,9 @@ lib.PreferenceManager = function(storage, opt_prefix) { this.trace = false; - var prefix = opt_prefix || '/'; - if (!prefix.endsWith('/')) + if (!prefix.endsWith('/')) { prefix += '/'; + } this.prefix = prefix; @@ -2050,6 +2486,7 @@ lib.PreferenceManager = function(storage, opt_prefix) { // to elide redundant storage writes (for quota reasons). this.isImportingJson_ = false; + /** @type {!Object} */ this.prefRecords_ = {}; this.globalObservers_ = []; @@ -2077,6 +2514,8 @@ lib.PreferenceManager = function(storage, opt_prefix) { * be taken from the default value defined with the preference. * * Equality tests against this value MUST use '===' or '!==' to be accurate. + * + * @type {symbol} */ lib.PreferenceManager.prototype.DEFAULT_VALUE = Symbol('DEFAULT_VALUE'); @@ -2085,6 +2524,10 @@ lib.PreferenceManager.prototype.DEFAULT_VALUE = Symbol('DEFAULT_VALUE'); * * These objects are managed by the PreferenceManager, you shouldn't need to * handle them directly. + * + * @param {string} name The name of the new preference (used for indexing). + * @param {*} defaultValue The default value for this preference. + * @constructor */ lib.PreferenceManager.Record = function(name, defaultValue) { this.name = name; @@ -2095,6 +2538,8 @@ lib.PreferenceManager.Record = function(name, defaultValue) { /** * A local copy of the DEFAULT_VALUE constant to make it less verbose. + * + * @type {symbol} */ lib.PreferenceManager.Record.prototype.DEFAULT_VALUE = lib.PreferenceManager.prototype.DEFAULT_VALUE; @@ -2102,9 +2547,9 @@ lib.PreferenceManager.Record.prototype.DEFAULT_VALUE = /** * Register a callback to be invoked when this preference changes. * - * @param {function(value, string, lib.PreferenceManager} observer The function - * to invoke. It will receive the new value, the name of the preference, - * and a reference to the PreferenceManager as parameters. + * @param {function(string, string, !lib.PreferenceManager)} observer The + * function to invoke. It will receive the new value, the name of the + * preference, and a reference to the PreferenceManager as parameters. */ lib.PreferenceManager.Record.prototype.addObserver = function(observer) { this.observers.push(observer); @@ -2113,21 +2558,26 @@ lib.PreferenceManager.Record.prototype.addObserver = function(observer) { /** * Unregister an observer callback. * - * @param {function} observer A previously registered callback. + * @param {function(string, string, !lib.PreferenceManager)} observer A + * previously registered callback. */ lib.PreferenceManager.Record.prototype.removeObserver = function(observer) { - var i = this.observers.indexOf(observer); - if (i >= 0) + const i = this.observers.indexOf(observer); + if (i >= 0) { this.observers.splice(i, 1); + } }; /** * Fetch the value of this preference. + * + * @return {*} The value for this preference. */ lib.PreferenceManager.Record.prototype.get = function() { if (this.currentValue === this.DEFAULT_VALUE) { - if (/^(string|number)$/.test(typeof this.defaultValue)) + if (/^(string|number)$/.test(typeof this.defaultValue)) { return this.defaultValue; + } if (typeof this.defaultValue == 'object') { // We want to return a COPY of the default value so that users can @@ -2148,8 +2598,9 @@ lib.PreferenceManager.Record.prototype.get = function() { * that you don't get notified about irrelevant changes. */ lib.PreferenceManager.prototype.deactivate = function() { - if (!this.isActive_) + if (!this.isActive_) { throw new Error('Not activated'); + } this.isActive_ = false; this.storage.removeObserver(this.storageObserver_); @@ -2163,8 +2614,9 @@ lib.PreferenceManager.prototype.deactivate = function() { * it's automatically called as part of the constructor. */ lib.PreferenceManager.prototype.activate = function() { - if (this.isActive_) + if (this.isActive_) { throw new Error('Already activated'); + } this.isActive_ = true; this.storage.addObserver(this.storageObserver_); @@ -2183,31 +2635,34 @@ lib.PreferenceManager.prototype.activate = function() { * This function is asynchronous, if you need to read preference values, you * *must* wait for the callback. * - * @param {function()} opt_callback Optional function to invoke when the read + * @param {function()=} callback Optional function to invoke when the read * has completed. */ -lib.PreferenceManager.prototype.readStorage = function(opt_callback) { - var pendingChildren = 0; +lib.PreferenceManager.prototype.readStorage = function(callback = undefined) { + let pendingChildren = 0; function onChildComplete() { - if (--pendingChildren == 0 && opt_callback) - opt_callback(); + if (--pendingChildren == 0 && callback) { + callback(); + } } - var keys = Object.keys(this.prefRecords_).map((el) => this.prefix + el); + const keys = Object.keys(this.prefRecords_).map((el) => this.prefix + el); - if (this.trace) + if (this.trace) { console.log('Preferences read: ' + this.prefix); + } - this.storage.getItems(keys, function(items) { - var prefixLength = this.prefix.length; + this.storage.getItems(keys).then((items) => { + const prefixLength = this.prefix.length; - for (var key in items) { - var value = items[key]; - var name = key.substr(prefixLength); - var needSync = (name in this.childLists_ && - (JSON.stringify(value) != - JSON.stringify(this.prefRecords_[name].currentValue))); + for (const key in items) { + const value = items[key]; + const name = key.substr(prefixLength); + const needSync = ( + name in this.childLists_ && + (JSON.stringify(value) != + JSON.stringify(this.prefRecords_[name].currentValue))); this.prefRecords_[name].currentValue = value; @@ -2217,18 +2672,18 @@ lib.PreferenceManager.prototype.readStorage = function(opt_callback) { } } - if (pendingChildren == 0 && opt_callback){ - // OSC/OOD hack for Firefox because one callback, notifyAll, called during the Terminal's constructor - // *has* to execute after the Term.scrollPort_ has been initialized and painted, which is asynchronous - in FF only. - // See line 10495 where they register that event listener. If notifyAll() occurs *before* paintIframeContents_ - // an error will occur becuase we're chaning the property of something that is not initialized yet. This error - // halts the screen and attempts to avoid it through other hacks (checking for nulls, etc.) have proven unsuccessful. - // Find out more here: https://github.com/OSC/ood-shell/issues/64 - // - // hopefully 100ms is enough of a buffer time, testing usually shows a 10ms difference between the 2 events in question. - setTimeout(opt_callback, 100); + if (pendingChildren == 0 && callback) { + // OSC/OOD hack for Firefox because one callback, notifyAll, called during the Terminal's constructor + // *has* to execute after the Term.scrollPort_ has been initialized and painted, which is asynchronous - in FF only. + // See line 10495 where they register that event listener. If notifyAll() occurs *before* paintIframeContents_ + // an error will occur becuase we're chaning the property of something that is not initialized yet. This error + // halts the screen and attempts to avoid it through other hacks (checking for nulls, etc.) have proven unsuccessful. + // Find out more here: https://github.com/OSC/ood-shell/issues/64 + // + // hopefully 100ms is enough of a buffer time, testing usually shows a 10ms difference between the 2 events in question. + setTimeout(callback, 100); } - }.bind(this)); + }); }; /** @@ -2238,18 +2693,18 @@ lib.PreferenceManager.prototype.readStorage = function(opt_callback) { * * @param {string} name The name of the preference. This will be prefixed by * the prefix of this PreferenceManager before written to local storage. - * @param {string|number|boolean|Object|Array|null} value The default value of + * @param {string|number|boolean|!Object|!Array|null} value The default value of * this preference. Anything that can be represented in JSON is a valid * default value. - * @param {function(value, string, lib.PreferenceManager} opt_observer A + * @param {function(*, string, !lib.PreferenceManager)=} onChange A * function to invoke when the preference changes. It will receive the new * value, the name of the preference, and a reference to the * PreferenceManager as parameters. */ lib.PreferenceManager.prototype.definePreference = function( - name, value, opt_onChange) { + name, value, onChange = undefined) { - var record = this.prefRecords_[name]; + let record = this.prefRecords_[name]; if (record) { this.changeDefault(name, value); } else { @@ -2257,19 +2712,20 @@ lib.PreferenceManager.prototype.definePreference = function( new lib.PreferenceManager.Record(name, value); } - if (opt_onChange) - record.addObserver(opt_onChange); + if (onChange) { + record.addObserver(onChange); + } }; /** * Define multiple preferences with a single function call. * - * @param {Array} defaults An array of 3-element arrays. Each three element + * @param {!Array<*>} defaults An array of 3-element arrays. Each three element * array should contain the [key, value, onChange] parameters for a * preference. */ lib.PreferenceManager.prototype.definePreferences = function(defaults) { - for (var i = 0; i < defaults.length; i++) { + for (let i = 0; i < defaults.length; i++) { this.definePreference(defaults[i][0], defaults[i][1], defaults[i][2]); } }; @@ -2287,10 +2743,10 @@ lib.PreferenceManager.prototype.definePreferences = function(defaults) { * preference on this PreferenceManager used to store the ordered list of * child ids. It is also used in get/add/remove operations to identify the * list of children to operate on. - * @param {function} childFactory A function that will be used to generate - * instances of these children. The factory function will receive the - * parent lib.PreferenceManager object and a unique id for the new child - * preferences. + * @param {function(!lib.PreferenceManager, string)} childFactory A function + * that will be used to generate instances of these children. The factory + * function will receive the parent lib.PreferenceManager object and a + * unique id for the new child preferences. */ lib.PreferenceManager.prototype.defineChildren = function( listName, childFactory) { @@ -2305,27 +2761,56 @@ lib.PreferenceManager.prototype.defineChildren = function( /** * Register to observe preference changes. * - * @param {Function} global A callback that will happen for every preference. + * @param {string} name The name of preference you wish to observe.. + * @param {function(*, string, !lib.PreferenceManager)} observer The callback. + */ +lib.PreferenceManager.prototype.addObserver = function(name, observer) { + if (!(name in this.prefRecords_)) { + throw new Error(`Unknown preference: ${name}`); + } + + this.prefRecords_[name].addObserver(observer); +}; + +/** + * Register to observe preference changes. + * + * @param {?function()} global A callback that will happen for every preference. * Pass null if you don't need one. - * @param {Object} map A map of preference specific callbacks. Pass null if + * @param {!Object} map A map of preference specific callbacks. Pass null if * you don't need any. */ lib.PreferenceManager.prototype.addObservers = function(global, map) { - if (global && typeof global != 'function') + if (global && typeof global != 'function') { throw new Error('Invalid param: globals'); + } - if (global) + if (global) { this.globalObservers_.push(global); + } - if (!map) + if (!map) { return; + } - for (var name in map) { - if (!(name in this.prefRecords_)) - throw new Error('Unknown preference: ' + name); + for (const name in map) { + this.addObserver(name, map[name]); + } +}; - this.prefRecords_[name].addObserver(map[name]); +/** + * Remove preference observer. + * + * @param {string} name The name of preference you wish to stop observing. + * @param {function(*, string, !lib.PreferenceManager)} observer The observer to + * remove. + */ +lib.PreferenceManager.prototype.removeObserver = function(name, observer) { + if (!(name in this.prefRecords_)) { + throw new Error(`Unknown preference: ${name}`); } + + this.prefRecords_[name].removeObserver(observer); }; /** @@ -2338,7 +2823,7 @@ lib.PreferenceManager.prototype.addObservers = function(global, map) { * a live object, for example when switching to a different prefix. */ lib.PreferenceManager.prototype.notifyAll = function() { - for (var name in this.prefRecords_) { + for (const name in this.prefRecords_) { this.notifyChange_(name); } }; @@ -2349,16 +2834,18 @@ lib.PreferenceManager.prototype.notifyAll = function() { * @param {string} name The name of the preference that changed. */ lib.PreferenceManager.prototype.notifyChange_ = function(name) { - var record = this.prefRecords_[name]; - if (!record) + const record = this.prefRecords_[name]; + if (!record) { throw new Error('Unknown preference: ' + name); + } - var currentValue = record.get(); + const currentValue = record.get(); - for (var i = 0; i < this.globalObservers_.length; i++) + for (let i = 0; i < this.globalObservers_.length; i++) { this.globalObservers_[i](name, currentValue); + } - for (var i = 0; i < record.observers.length; i++) { + for (let i = 0; i < record.observers.length; i++) { record.observers[i](currentValue, name, this); } }; @@ -2371,30 +2858,31 @@ lib.PreferenceManager.prototype.notifyChange_ = function(name) { * and use it. * * @param {string} listName The child list to create the new instance from. - * @param {string} opt_hint Optional hint to include in the child id. - * @param {string} opt_id Optional id to override the generated id. + * @param {?string=} hint Optional hint to include in the child id. + * @param {string=} id Optional id to override the generated id. + * @return {!lib.PreferenceManager} The new child preference manager. */ -lib.PreferenceManager.prototype.createChild = function(listName, opt_hint, - opt_id) { - var ids = this.get(listName); - var id; +lib.PreferenceManager.prototype.createChild = function( + listName, hint = undefined, id = undefined) { + const ids = /** @type {!Array} */ (this.get(listName)); - if (opt_id) { - id = opt_id; - if (ids.indexOf(id) != -1) + if (id) { + if (ids.indexOf(id) != -1) { throw new Error('Duplicate child: ' + listName + ': ' + id); + } } else { // Pick a random, unique 4-digit hex identifier for the new profile. while (!id || ids.indexOf(id) != -1) { id = lib.f.randomInt(1, 0xffff).toString(16); id = lib.f.zpad(id, 4); - if (opt_hint) - id = opt_hint + ':' + id; + if (hint) { + id = `${hint}:${id}`; + } } } - var childManager = this.childFactories_[listName](this, id); + const childManager = this.childFactories_[listName](this, id); childManager.trace = this.trace; childManager.resetAll(); @@ -2416,11 +2904,11 @@ lib.PreferenceManager.prototype.createChild = function(listName, opt_hint, * @param {string} id The child ID. */ lib.PreferenceManager.prototype.removeChild = function(listName, id) { - var prefs = this.getChild(listName, id); + const prefs = this.getChild(listName, id); prefs.resetAll(); - var ids = this.get(listName); - var i = ids.indexOf(id); + const ids = /** @type {!Array} */ (this.get(listName)); + const i = ids.indexOf(id); if (i != -1) { ids.splice(i, 1); this.set(listName, ids, undefined, !this.isImportingJson_); @@ -2437,66 +2925,26 @@ lib.PreferenceManager.prototype.removeChild = function(listName, id) { * * @param {string} listName The child list to look in. * @param {string} id The child ID. - * @param {*} opt_default The optional default value to return if the child - * is not found. + * @param {!lib.PreferenceManager=} defaultValue The value to return if the + * child is not found. + * @return {!lib.PreferenceManager} The specified child PreferenceManager. */ -lib.PreferenceManager.prototype.getChild = function(listName, id, opt_default) { - if (!(listName in this.childLists_)) +lib.PreferenceManager.prototype.getChild = function( + listName, id, defaultValue = undefined) { + if (!(listName in this.childLists_)) { throw new Error('Unknown child list: ' + listName); + } - var childList = this.childLists_[listName]; + const childList = this.childLists_[listName]; if (!(id in childList)) { - if (typeof opt_default == 'undefined') + if (defaultValue === undefined) { throw new Error('Unknown "' + listName + '" child: ' + id); - - return opt_default; - } - - return childList[id]; -}; - -/** - * Calculate the difference between two lists of child ids. - * - * Given two arrays of child ids, this function will return an object - * with "added", "removed", and "common" properties. Each property is - * a map of child-id to `true`. For example, given... - * - * a = ['child-x', 'child-y'] - * b = ['child-y'] - * - * diffChildLists(a, b) => - * { added: { 'child-x': true }, removed: {}, common: { 'child-y': true } } - * - * The added/removed properties assume that `a` is the current list. - * - * @param {Array[string]} a The most recent list of child ids. - * @param {Array[string]} b An older list of child ids. - * @return {Object} An object with added/removed/common properties. - */ -lib.PreferenceManager.diffChildLists = function(a, b) { - var rv = { - added: {}, - removed: {}, - common: {}, - }; - - for (var i = 0; i < a.length; i++) { - if (b.indexOf(a[i]) != -1) { - rv.common[a[i]] = true; - } else { - rv.added[a[i]] = true; } - } - - for (var i = 0; i < b.length; i++) { - if ((b[i] in rv.added) || (b[i] in rv.common)) - continue; - rv.removed[b[i]] = true; + return defaultValue; } - return rv; + return childList[id]; }; /** @@ -2508,36 +2956,34 @@ lib.PreferenceManager.diffChildLists = function(a, b) { * be deleted. * * @param {string} listName The child list to synchronize. - * @param {function()} opt_callback Optional function to invoke when the sync - * is complete. + * @param {function()=} callback Function to invoke when the sync finishes. */ lib.PreferenceManager.prototype.syncChildList = function( - listName, opt_callback) { - - var pendingChildren = 0; + listName, callback = undefined) { + let pendingChildren = 0; function onChildStorage() { - if (--pendingChildren == 0 && opt_callback) - opt_callback(); + if (--pendingChildren == 0 && callback) { + callback(); + } } // The list of child ids that we *should* have a manager for. - var currentIds = this.get(listName); + const currentIds = /** @type {!Array} */ (this.get(listName)); // The known managers at the start of the sync. Any manager still in this // list at the end should be discarded. - var oldIds = Object.keys(this.childLists_[listName]); + const oldIds = Object.keys(this.childLists_[listName]); - var rv = lib.PreferenceManager.diffChildLists(currentIds, oldIds); + for (let i = 0; i < currentIds.length; i++) { + const id = currentIds[i]; - for (var i = 0; i < currentIds.length; i++) { - var id = currentIds[i]; - - var managerIndex = oldIds.indexOf(id); - if (managerIndex >= 0) + const managerIndex = oldIds.indexOf(id); + if (managerIndex >= 0) { oldIds.splice(managerIndex, 1); + } if (!this.childLists_[listName][id]) { - var childManager = this.childFactories_[listName](this, id); + const childManager = this.childFactories_[listName](this, id); if (!childManager) { console.warn('Unable to restore child: ' + listName + ': ' + id); continue; @@ -2550,12 +2996,13 @@ lib.PreferenceManager.prototype.syncChildList = function( } } - for (var i = 0; i < oldIds.length; i++) { + for (let i = 0; i < oldIds.length; i++) { delete this.childLists_[listName][oldIds[i]]; } - if (!pendingChildren && opt_callback) - setTimeout(opt_callback); + if (!pendingChildren && callback) { + setTimeout(callback); + } }; /** @@ -2567,9 +3014,10 @@ lib.PreferenceManager.prototype.syncChildList = function( * @param {string} name The preference to reset. */ lib.PreferenceManager.prototype.reset = function(name) { - var record = this.prefRecords_[name]; - if (!record) + const record = this.prefRecords_[name]; + if (!record) { throw new Error('Unknown preference: ' + name); + } this.storage.removeItem(this.prefix + name); @@ -2583,23 +3031,23 @@ lib.PreferenceManager.prototype.reset = function(name) { * Reset all preferences back to their default state. */ lib.PreferenceManager.prototype.resetAll = function() { - var changed = []; + const changed = []; - for (var listName in this.childLists_) { - var childList = this.childLists_[listName]; - for (var id in childList) { + for (const listName in this.childLists_) { + const childList = this.childLists_[listName]; + for (const id in childList) { childList[id].resetAll(); } } - for (var name in this.prefRecords_) { + for (const name in this.prefRecords_) { if (this.prefRecords_[name].currentValue !== this.DEFAULT_VALUE) { this.prefRecords_[name].currentValue = this.DEFAULT_VALUE; changed.push(name); } } - var keys = Object.keys(this.prefRecords_).map(function(el) { + const keys = Object.keys(this.prefRecords_).map(function(el) { return this.prefix + el; }.bind(this)); @@ -2620,6 +3068,7 @@ lib.PreferenceManager.prototype.resetAll = function() { * * @param {*} a A value to compare. * @param {*} b A value to compare. + * @return {boolean} Whether the two are not equal. */ lib.PreferenceManager.prototype.diff = function(a, b) { // If the types are different. @@ -2654,9 +3103,10 @@ lib.PreferenceManager.prototype.diff = function(a, b) { * @param {*} newValue The new default value for the preference. */ lib.PreferenceManager.prototype.changeDefault = function(name, newValue) { - var record = this.prefRecords_[name]; - if (!record) + const record = this.prefRecords_[name]; + if (!record) { throw new Error('Unknown preference: ' + name); + } if (!this.diff(record.defaultValue, newValue)) { // Default value hasn't changed. @@ -2677,11 +3127,11 @@ lib.PreferenceManager.prototype.changeDefault = function(name, newValue) { /** * Change the default value of multiple preferences. * - * @param {Object} map A map of name -> value pairs specifying the new default + * @param {!Object} map A map of name -> value pairs specifying the new default * values. */ lib.PreferenceManager.prototype.changeDefaults = function(map) { - for (var key in map) { + for (const key in map) { this.changeDefault(key, map[key]); } }; @@ -2698,26 +3148,32 @@ lib.PreferenceManager.prototype.changeDefaults = function(map) { * @param {function()=} onComplete Callback when the set call completes. * @param {boolean=} saveToStorage Whether to commit the change to the backing * storage or only the in-memory record copy. + * @return {!Promise} Promise which resolves once all observers are + * notified. */ lib.PreferenceManager.prototype.set = function( - name, newValue, onComplete=undefined, saveToStorage=true) { - var record = this.prefRecords_[name]; - if (!record) + name, newValue, onComplete = undefined, saveToStorage = true) { + const record = this.prefRecords_[name]; + if (!record) { throw new Error('Unknown preference: ' + name); + } - var oldValue = record.get(); + const oldValue = record.get(); - if (!this.diff(oldValue, newValue)) - return; + if (!this.diff(oldValue, newValue)) { + return Promise.resolve(); + } if (this.diff(record.defaultValue, newValue)) { record.currentValue = newValue; - if (saveToStorage) - this.storage.setItem(this.prefix + name, newValue, onComplete); + if (saveToStorage) { + this.storage.setItem(this.prefix + name, newValue).then(onComplete); + } } else { record.currentValue = this.DEFAULT_VALUE; - if (saveToStorage) - this.storage.removeItem(this.prefix + name, onComplete); + if (saveToStorage) { + this.storage.removeItem(this.prefix + name).then(onComplete); + } } // We need to manually send out the notification on this instance. If we @@ -2726,45 +3182,103 @@ lib.PreferenceManager.prototype.set = function( // currentValue until the storage event, a pref read immediately after a write // would return the previous value. // - // The notification is in a timeout so clients don't accidentally depend on + // The notification is async so clients don't accidentally depend on // a synchronous notification. - setTimeout(this.notifyChange_.bind(this, name), 0); + return Promise.resolve().then(() => { + this.notifyChange_(name); + }); }; /** * Get the value of a preference. * - * @param {string} key The preference to get. + * @param {string} name The preference to get. + * @return {*} The preference's value. */ lib.PreferenceManager.prototype.get = function(name) { - var record = this.prefRecords_[name]; - if (!record) + const record = this.prefRecords_[name]; + if (!record) { throw new Error('Unknown preference: ' + name); + } return record.get(); }; +/** + * Get the default value of a preference. + * + * @param {string} name The preference to get. + * @return {*} The preference's default value. + */ +lib.PreferenceManager.prototype.getDefault = function(name) { + const record = this.prefRecords_[name]; + if (!record) { + throw new Error(`Unknown preference: ${name}`); + } + + return record.defaultValue; +}; + +/** + * Get the boolean value of a preference. + * + * @param {string} name The preference to get. + * @return {boolean} + */ +lib.PreferenceManager.prototype.getBoolean = function(name) { + const result = this.get(name); + lib.assert(typeof result == 'boolean'); + return result; +}; + +/** + * Get the number value of a preference. + * + * @param {string} name The preference to get. + * @return {number} + */ +lib.PreferenceManager.prototype.getNumber = function(name) { + const result = this.get(name); + lib.assert(typeof result == 'number'); + return result; +}; + +/** + * Get the string value of a preference. + * + * @param {string} name The preference to get. + * @return {string} + */ +lib.PreferenceManager.prototype.getString = function(name) { + const result = this.get(name); + lib.assert(typeof result == 'string'); + return result; +}; + /** * Return all non-default preferences as a JSON object. * * This includes any nested preference managers as well. + * + * @return {!Object} The JSON preferences. */ lib.PreferenceManager.prototype.exportAsJson = function() { - var rv = {}; + const rv = {}; - for (var name in this.prefRecords_) { + for (const name in this.prefRecords_) { if (name in this.childLists_) { rv[name] = []; - var childIds = this.get(name); - for (var i = 0; i < childIds.length; i++) { - var id = childIds[i]; + const childIds = /** @type {!Array} */ (this.get(name)); + for (let i = 0; i < childIds.length; i++) { + const id = childIds[i]; rv[name].push({id: id, json: this.getChild(name, id).exportAsJson()}); } } else { - var record = this.prefRecords_[name]; - if (record.currentValue != this.DEFAULT_VALUE) + const record = this.prefRecords_[name]; + if (record.currentValue != this.DEFAULT_VALUE) { rv[name] = record.currentValue; + } } } @@ -2775,34 +3289,41 @@ lib.PreferenceManager.prototype.exportAsJson = function() { * Import a JSON blob of preferences previously generated with exportAsJson. * * This will create nested preference managers as well. + * + * @param {!Object} json The JSON settings to import. + * @param {function()=} onComplete Callback when all imports have finished. */ -lib.PreferenceManager.prototype.importFromJson = function(json, opt_onComplete) { +lib.PreferenceManager.prototype.importFromJson = function(json, onComplete) { this.isImportingJson_ = true; let pendingWrites = 0; const onWriteStorage = () => { if (--pendingWrites < 1) { - if (opt_onComplete) - opt_onComplete(); + if (onComplete) { + onComplete(); + } // We've delayed updates to the child arrays, so flush them now. - for (let name in json) - if (name in this.childLists_) + for (const name in json) { + if (name in this.childLists_) { this.set(name, this.get(name)); + } + } this.isImportingJson_ = false; } }; - for (var name in json) { + for (const name in json) { if (name in this.childLists_) { - var childList = json[name]; - for (var i = 0; i < childList.length; i++) { - var id = childList[i].id; + const childList = json[name]; + for (let i = 0; i < childList.length; i++) { + const id = childList[i].id; - var childPrefManager = this.childLists_[name][id]; - if (!childPrefManager) + let childPrefManager = this.childLists_[name][id]; + if (!childPrefManager) { childPrefManager = this.createChild(name, null, id); + } childPrefManager.importFromJson(childList[i].json, onWriteStorage); pendingWrites++; @@ -2816,12 +3337,15 @@ lib.PreferenceManager.prototype.importFromJson = function(json, opt_onComplete) // If we didn't update any children, no async work has been queued, so make // the completion callback directly. - if (pendingWrites == 0 && opt_onComplete) - opt_onComplete(); + if (pendingWrites == 0 && onComplete) { + onComplete(); + } }; /** * Called when one of the child list preferences changes. + * + * @param {string} listName The child list to synchronize. */ lib.PreferenceManager.prototype.onChildListChange_ = function(listName) { this.syncChildList(listName); @@ -2829,27 +3353,31 @@ lib.PreferenceManager.prototype.onChildListChange_ = function(listName) { /** * Called when a key in the storage changes. + * + * @param {!Object} map Dictionary of changed settings. */ lib.PreferenceManager.prototype.onStorageChange_ = function(map) { - for (var key in map) { + for (const key in map) { if (this.prefix) { - if (key.lastIndexOf(this.prefix, 0) != 0) + if (key.lastIndexOf(this.prefix, 0) != 0) { continue; + } } - var name = key.substr(this.prefix.length); + const name = key.substr(this.prefix.length); if (!(name in this.prefRecords_)) { // Sometimes we'll get notified about prefs that are no longer defined. continue; } - var record = this.prefRecords_[name]; + const record = this.prefRecords_[name]; - var newValue = map[key].newValue; - var currentValue = record.currentValue; - if (currentValue === record.DEFAULT_VALUE) - currentValue = (void 0); + const newValue = map[key].newValue; + let currentValue = record.currentValue; + if (currentValue === record.DEFAULT_VALUE) { + currentValue = undefined; + } if (this.diff(currentValue, newValue)) { if (typeof newValue == 'undefined' || newValue === null) { @@ -2879,23 +3407,26 @@ lib.PreferenceManager.prototype.onStorageChange_ = function(map) { * collisions. */ lib.resource = { - resources_: {} + resources_: {}, }; +/** @typedef {{type: string, name: string, data: *}} */ +lib.resource.ResourceRecord; + /** * Add a resource. * * @param {string} name A name for the resource. You should prefix this to - * avoid collisions with resources from a shared library. + * avoid collisions with resources from a shared library. * @param {string} type A mime type for the resource, or "raw" if not - * applicable. + * applicable. * @param {*} data The value of the resource. */ lib.resource.add = function(name, type, data) { lib.resource.resources_[name] = { type: type, name: name, - data: data + data: data, }; }; @@ -2905,16 +3436,14 @@ lib.resource.add = function(name, type, data) { * The resource data is stored on the "data" property of the returned object. * * @param {string} name The name of the resource to get. - * @param {*} opt_defaultValue The optional value to return if the resource is - * not defined. - * @return {object} An object with "type", "name", and "data" properties. + * @param {!lib.resource.ResourceRecord=} defaultValue The value to return if + * the resource is not defined. + * @return {!lib.resource.ResourceRecord} The matching resource if it exists. */ -lib.resource.get = function(name, opt_defaultValue) { +lib.resource.get = function(name, defaultValue) { if (!(name in lib.resource.resources_)) { - if (typeof opt_defaultValue == 'undefined') - throw 'Unknown resource: ' + name; - - return opt_defaultValue; + lib.assert(defaultValue !== undefined); + return defaultValue; } return lib.resource.resources_[name]; @@ -2924,16 +3453,12 @@ lib.resource.get = function(name, opt_defaultValue) { * Retrieve resource data. * * @param {string} name The name of the resource to get. - * @param {*} opt_defaultValue The optional value to return if the resource is - * not defined. + * @param {*=} defaultValue The value to return if the resource is not defined. * @return {*} The resource data. */ -lib.resource.getData = function(name, opt_defaultValue) { +lib.resource.getData = function(name, defaultValue) { if (!(name in lib.resource.resources_)) { - if (typeof opt_defaultValue == 'undefined') - throw 'Unknown resource: ' + name; - - return opt_defaultValue; + return defaultValue; } return lib.resource.resources_[name].data; @@ -2943,12 +3468,12 @@ lib.resource.getData = function(name, opt_defaultValue) { * Retrieve resource as a data: url. * * @param {string} name The name of the resource to get. - * @param {*} opt_defaultValue The optional value to return if the resource is - * not defined. - * @return {*} A data: url encoded version of the resource. + * @param {!lib.resource.ResourceRecord=} defaultValue The value to return if + * the resource is not defined. + * @return {string} A data: url encoded version of the resource. */ -lib.resource.getDataUrl = function(name, opt_defaultValue) { - var resource = lib.resource.get(name, opt_defaultValue); +lib.resource.getDataUrl = function(name, defaultValue) { + const resource = lib.resource.get(name, defaultValue); return 'data:' + resource.type + ',' + resource.data; }; // SOURCE FILE: libdot/js/lib_storage.js @@ -2959,160 +3484,286 @@ lib.resource.getDataUrl = function(name, opt_defaultValue) { /** * Namespace for implementations of persistent, possibly cloud-backed * storage. + * + * @interface */ -lib.Storage = new Object(); -// SOURCE FILE: libdot/js/lib_storage_chrome.js -// Copyright (c) 2012 The Chromium OS Authors. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -/** - * chrome.storage based class with an async interface that is interchangeable - * with other lib.Storage.* implementations. - */ -lib.Storage.Chrome = function(storage) { - this.storage_ = storage; - this.observers_ = []; - - chrome.storage.onChanged.addListener(this.onChanged_.bind(this)); -}; - -/** - * Called by the storage implementation when the storage is modified. - */ -lib.Storage.Chrome.prototype.onChanged_ = function(changes, areaname) { - if (chrome.storage[areaname] != this.storage_) - return; - - for (var i = 0; i < this.observers_.length; i++) { - this.observers_[i](changes); - } -}; +lib.Storage = function() {}; /** * Register a function to observe storage changes. * - * @param {function(map)} callback The function to invoke when the storage - * changes. + * @param {function(!Object)} callback The function to + * invoke when the storage changes. */ -lib.Storage.Chrome.prototype.addObserver = function(callback) { - this.observers_.push(callback); -}; +lib.Storage.prototype.addObserver = function(callback) {}; /** * Unregister a change observer. * - * @param {function} observer A previously registered callback. + * @param {function(!Object)} callback A previously + * registered callback. */ -lib.Storage.Chrome.prototype.removeObserver = function(callback) { - var i = this.observers_.indexOf(callback); - if (i != -1) - this.observers_.splice(i, 1); -}; +lib.Storage.prototype.removeObserver = function(callback) {}; /** * Delete everything in this storage. - * - * @param {function(map)} callback The function to invoke when the delete - * has completed. */ -lib.Storage.Chrome.prototype.clear = function(opt_callback) { - this.storage_.clear(); - - if (opt_callback) - setTimeout(opt_callback, 0); -}; +lib.Storage.prototype.clear = async function() {}; /** * Return the current value of a storage item. * * @param {string} key The key to look up. - * @param {function(value) callback The function to invoke when the value has - * been retrieved. + * @return {!Promise<*>} A promise resolving to the requested item. */ -lib.Storage.Chrome.prototype.getItem = function(key, callback) { - this.storage_.get(key, callback); -}; +lib.Storage.prototype.getItem = async function(key) {}; + /** * Fetch the values of multiple storage items. * - * @param {Array} keys The keys to look up. - * @param {function(map) callback The function to invoke when the values have - * been retrieved. + * @param {?Array} keys The keys to look up. Pass null for all keys. + * @return {!Promise>} A promise resolving to the requested + * items. */ - -lib.Storage.Chrome.prototype.getItems = function(keys, callback) { - this.storage_.get(keys, callback); -}; +lib.Storage.prototype.getItems = async function(keys) {}; /** * Set a value in storage. * + * You don't have to wait for the set to complete in order to read the value + * since the local cache is updated synchronously. + * * @param {string} key The key for the value to be stored. * @param {*} value The value to be stored. Anything that can be serialized * with JSON is acceptable. - * @param {function()} opt_callback Optional function to invoke when the - * set is complete. You don't have to wait for the set to complete in order - * to read the value, since the local cache is updated synchronously. - */ -lib.Storage.Chrome.prototype.setItem = function(key, value, opt_callback) { - const onComplete = () => { - const err = lib.f.lastError(); - if (err) { - // Doesn't seem to be any better way of handling this. - // https://crbug.com/764759 - if (err.indexOf('MAX_WRITE_OPERATIONS')) { - console.warn(`Will retry save of ${key} after exceeding quota: ${err}`); - setTimeout(() => this.setItem(key, value, onComplete), 1000); - return; - } else { - console.error(`Unknown runtime error: ${err}`); + */ +lib.Storage.prototype.setItem = async function(key, value) {}; + +/** + * Set multiple values in storage. + * + * You don't have to wait for the set to complete in order to read the value + * since the local cache is updated synchronously. + * + * @param {!Object} obj A map of key/values to set in storage. + */ +lib.Storage.prototype.setItems = async function(obj) {}; + +/** + * Remove an item from storage. + * + * @param {string} key The key to be removed. + */ +lib.Storage.prototype.removeItem = async function(key) {}; + +/** + * Remove multiple items from storage. + * + * @param {!Array} keys The keys to be removed. + */ +lib.Storage.prototype.removeItems = async function(keys) {}; + +/** + * Create the set of changes between two states. + * + * This is used to synthesize the equivalent of Chrome's StorageEvent for use + * by our stub APIs and testsuites. We expect Chrome's StorageEvent to also + * match the web's Storage API & window.onstorage events. + * + * @param {!Object} oldStorage The old storage state. + * @param {!Object} newStorage The new storage state. + * @return {!Object} The changes. + */ +lib.Storage.generateStorageChanges = function(oldStorage, newStorage) { + const changes = {}; + + // See what's changed. + for (const key in newStorage) { + const newValue = newStorage[key]; + if (oldStorage.hasOwnProperty(key)) { + // Key has been updated. + const oldValue = oldStorage[key]; + if (oldValue !== newValue) { + changes[key] = {oldValue, newValue}; } + } else { + // Key has been added. + changes[key] = {newValue}; } + } - if (opt_callback) - opt_callback(); - }; + // See what's deleted. + for (const key in oldStorage) { + if (!newStorage.hasOwnProperty(key)) { + changes[key] = {oldValue: oldStorage[key]}; + } + } + + return changes; +}; +// SOURCE FILE: libdot/js/lib_storage_chrome.js +// Copyright (c) 2012 The Chromium OS Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +/** + * chrome.storage based class with an async interface that is interchangeable + * with other lib.Storage.* implementations. + * + * @param {!StorageArea} storage The backing storage. + * @implements {lib.Storage} + * @constructor + */ +lib.Storage.Chrome = function(storage) { + this.storage_ = storage; + this.observers_ = []; + + // R73 introduced onChanged for the storage area. Keep backwards compat + // for EOL systems until we drop app support completely. + if (storage.onChanged) { + // Newer R73+ code. + storage.onChanged.addListener(this.onChanged_.bind(this)); + } else { + // Older { + if (chrome.storage[areaname] === this.storage_) { + this.onChanged_(changes); + } + }); + } +}; + +/** + * Called by the storage implementation when the storage is modified. + * + * @param {!Object} changes Object mapping each key that + * changed to its corresponding StorageChange for that item. + */ +lib.Storage.Chrome.prototype.onChanged_ = function(changes) { + this.observers_.forEach((o) => o(changes)); +}; + +/** + * Register a function to observe storage changes. + * + * @param {function(!Object)} callback The function to + * invoke when the storage changes. + * @override + */ +lib.Storage.Chrome.prototype.addObserver = function(callback) { + this.observers_.push(callback); +}; + +/** + * Unregister a change observer. + * + * @param {function(!Object)} callback A previously + * registered callback. + * @override + */ +lib.Storage.Chrome.prototype.removeObserver = function(callback) { + const i = this.observers_.indexOf(callback); + if (i != -1) { + this.observers_.splice(i, 1); + } +}; + +/** + * Delete everything in this storage. + * + * @override + */ +lib.Storage.Chrome.prototype.clear = async function() { + return new Promise((resolve) => { + this.storage_.clear(resolve); + }); +}; + +/** + * Return the current value of a storage item. + * + * @param {string} key The key to look up. + * @override + */ +lib.Storage.Chrome.prototype.getItem = async function(key) { + return this.getItems([key]).then((items) => items[key]); +}; + +/** + * Fetch the values of multiple storage items. + * + * @param {?Array} keys The keys to look up. Pass null for all keys. + * @override + */ +lib.Storage.Chrome.prototype.getItems = async function(keys) { + return new Promise((resolve) => { + this.storage_.get(keys, resolve); + }); +}; + +/** + * Set a value in storage. + * + * @param {string} key The key for the value to be stored. + * @param {*} value The value to be stored. Anything that can be serialized + * with JSON is acceptable. + * @override + */ +lib.Storage.Chrome.prototype.setItem = async function(key, value) { + return new Promise((resolve) => { + const onComplete = () => { + const err = lib.f.lastError(); + if (err) { + // Doesn't seem to be any better way of handling this. + // https://crbug.com/764759 + if (err.indexOf('MAX_WRITE_OPERATIONS')) { + console.warn(`Will retry '${key}' save after exceeding quota:`, err); + setTimeout(() => this.setItem(key, value).then(onComplete), 1000); + return; + } else { + console.error(`Unknown runtime error: ${err}`); + } + } + + resolve(); + }; - var obj = {}; - obj[key] = value; - this.storage_.set(obj, onComplete); + this.setItems({[key]: value}).then(onComplete); + }); }; /** * Set multiple values in storage. * - * @param {Object} map A map of key/values to set in storage. - * @param {function()} opt_callback Optional function to invoke when the - * set is complete. You don't have to wait for the set to complete in order - * to read the value, since the local cache is updated synchronously. + * @param {!Object} obj A map of key/values to set in storage. + * @override */ -lib.Storage.Chrome.prototype.setItems = function(obj, opt_callback) { - this.storage_.set(obj, opt_callback); +lib.Storage.Chrome.prototype.setItems = async function(obj) { + return new Promise((resolve) => { + this.storage_.set(obj, resolve); + }); }; /** * Remove an item from storage. * * @param {string} key The key to be removed. - * @param {function()} opt_callback Optional function to invoke when the - * remove is complete. You don't have to wait for the set to complete in - * order to read the value, since the local cache is updated synchronously. + * @override */ -lib.Storage.Chrome.prototype.removeItem = function(key, opt_callback) { - this.storage_.remove(key, opt_callback); +lib.Storage.Chrome.prototype.removeItem = async function(key) { + return this.removeItems([key]); }; /** * Remove multiple items from storage. * - * @param {Array} keys The keys to be removed. - * @param {function()} opt_callback Optional function to invoke when the - * remove is complete. You don't have to wait for the set to complete in - * order to read the value, since the local cache is updated synchronously. + * @param {!Array} keys The keys to be removed. + * @override */ -lib.Storage.Chrome.prototype.removeItems = function(keys, opt_callback) { - this.storage_.remove(keys, opt_callback); +lib.Storage.Chrome.prototype.removeItems = async function(keys) { + return new Promise((resolve) => { + this.storage_.remove(keys, resolve); + }); }; // SOURCE FILE: libdot/js/lib_storage_local.js // Copyright (c) 2012 The Chromium OS Authors. All rights reserved. @@ -3122,32 +3773,55 @@ lib.Storage.Chrome.prototype.removeItems = function(keys, opt_callback) { /** * window.localStorage based class with an async interface that is * interchangeable with other lib.Storage.* implementations. + * + * @param {!Storage=} storage The backing storage. + * @implements {lib.Storage} + * @constructor */ -lib.Storage.Local = function() { +lib.Storage.Local = function(storage = undefined) { this.observers_ = []; - this.storage_ = window.localStorage; - window.addEventListener('storage', this.onStorage_.bind(this)); + /** @type {!Storage} */ + this.storage_ = storage ? storage : lib.notNull(window.localStorage); + // Closure thinks all addEventListener calls take Events. + window.addEventListener( + 'storage', + /** @type {function(!Event)} */ (this.onStorage_.bind(this))); +}; + +/** + * Returns parsed JSON, or original value if JSON.parse fails. + * + * @param {?string} jsonString The string to parse. + * @return {*} + */ +lib.Storage.Local.prototype.parseJson_ = function(jsonString) { + if (jsonString !== null) { + try { + return JSON.parse(jsonString); + } catch (e) { + // Ignore and return jsonString. + } + } + return jsonString; }; /** * Called by the storage implementation when the storage is modified. + * + * @param {!StorageEvent} e The setting that has changed. */ lib.Storage.Local.prototype.onStorage_ = function(e) { - if (e.storageArea != this.storage_) + if (e.storageArea != this.storage_) { return; + } - // JS throws an exception if JSON.parse is given an empty string. So here we - // only parse if the value is truthy. This mean the empty string, undefined - // and null will not be parsed. - var prevValue = e.oldValue ? JSON.parse(e.oldValue) : e.oldValue; - var curValue = e.newValue ? JSON.parse(e.newValue) : e.newValue; - var o = {}; + const o = {}; o[e.key] = { - oldValue: prevValue, - newValue: curValue + oldValue: this.parseJson_(e.oldValue), + newValue: this.parseJson_(e.newValue), }; - for (var i = 0; i < this.observers_.length; i++) { + for (let i = 0; i < this.observers_.length; i++) { this.observers_[i](o); } }; @@ -3155,8 +3829,9 @@ lib.Storage.Local.prototype.onStorage_ = function(e) { /** * Register a function to observe storage changes. * - * @param {function(map)} callback The function to invoke when the storage + * @param {function(!Object)} callback The function to invoke when the storage * changes. + * @override */ lib.Storage.Local.prototype.addObserver = function(callback) { this.observers_.push(callback); @@ -3165,74 +3840,67 @@ lib.Storage.Local.prototype.addObserver = function(callback) { /** * Unregister a change observer. * - * @param {function} observer A previously registered callback. + * @param {function(!Object)} callback A previously registered callback. + * @override */ lib.Storage.Local.prototype.removeObserver = function(callback) { - var i = this.observers_.indexOf(callback); - if (i != -1) + const i = this.observers_.indexOf(callback); + if (i != -1) { this.observers_.splice(i, 1); + } }; /** * Delete everything in this storage. * - * @param {function(map)} callback The function to invoke when the delete - * has completed. + * @override */ -lib.Storage.Local.prototype.clear = function(opt_callback) { +lib.Storage.Local.prototype.clear = async function() { this.storage_.clear(); - if (opt_callback) - setTimeout(opt_callback, 0); + // Force deferment for the standard API. + await 0; }; /** * Return the current value of a storage item. * * @param {string} key The key to look up. - * @param {function(value) callback The function to invoke when the value has - * been retrieved. + * @override */ -lib.Storage.Local.prototype.getItem = function(key, callback) { - var value = this.storage_.getItem(key); - - if (typeof value == 'string') { - try { - value = JSON.parse(value); - } catch (e) { - // If we can't parse the value, just return it unparsed. - } - } - - setTimeout(callback.bind(null, value), 0); +lib.Storage.Local.prototype.getItem = async function(key) { + return this.getItems([key]).then((items) => items[key]); }; /** * Fetch the values of multiple storage items. * - * @param {Array} keys The keys to look up. - * @param {function(map) callback The function to invoke when the values have - * been retrieved. + * @param {?Array} keys The keys to look up. Pass null for all keys. + * @override */ -lib.Storage.Local.prototype.getItems = function(keys, callback) { - var rv = {}; +lib.Storage.Local.prototype.getItems = async function(keys) { + const rv = {}; + if (!keys) { + keys = []; + for (let i = 0; i < this.storage_.length; i++) { + keys.push(this.storage_.key(i)); + } + } - for (var i = keys.length - 1; i >= 0; i--) { - var key = keys[i]; - var value = this.storage_.getItem(key); + for (let i = keys.length - 1; i >= 0; i--) { + const key = keys[i]; + const value = this.storage_.getItem(key); if (typeof value == 'string') { - try { - rv[key] = JSON.parse(value); - } catch (e) { - // If we can't parse the value, just return it unparsed. - rv[key] = value; - } + rv[key] = this.parseJson_(value); } else { keys.splice(i, 1); } } - setTimeout(callback.bind(null, rv), 0); + // Force deferment for the standard API. + await 0; + + return rv; }; /** @@ -3241,64 +3909,50 @@ lib.Storage.Local.prototype.getItems = function(keys, callback) { * @param {string} key The key for the value to be stored. * @param {*} value The value to be stored. Anything that can be serialized * with JSON is acceptable. - * @param {function()} opt_callback Optional function to invoke when the - * set is complete. You don't have to wait for the set to complete in order - * to read the value, since the local cache is updated synchronously. + * @override */ -lib.Storage.Local.prototype.setItem = function(key, value, opt_callback) { - this.storage_.setItem(key, JSON.stringify(value)); - - if (opt_callback) - setTimeout(opt_callback, 0); +lib.Storage.Local.prototype.setItem = async function(key, value) { + return this.setItems({[key]: value}); }; /** * Set multiple values in storage. * - * @param {Object} map A map of key/values to set in storage. - * @param {function()} opt_callback Optional function to invoke when the - * set is complete. You don't have to wait for the set to complete in order - * to read the value, since the local cache is updated synchronously. + * @param {!Object} obj A map of key/values to set in storage. + * @override */ -lib.Storage.Local.prototype.setItems = function(obj, opt_callback) { - for (var key in obj) { +lib.Storage.Local.prototype.setItems = async function(obj) { + for (const key in obj) { this.storage_.setItem(key, JSON.stringify(obj[key])); } - if (opt_callback) - setTimeout(opt_callback, 0); + // Force deferment for the standard API. + await 0; }; /** * Remove an item from storage. * * @param {string} key The key to be removed. - * @param {function()} opt_callback Optional function to invoke when the - * remove is complete. You don't have to wait for the set to complete in - * order to read the value, since the local cache is updated synchronously. + * @override */ -lib.Storage.Local.prototype.removeItem = function(key, opt_callback) { - this.storage_.removeItem(key); - - if (opt_callback) - setTimeout(opt_callback, 0); +lib.Storage.Local.prototype.removeItem = async function(key) { + return this.removeItems([key]); }; /** * Remove multiple items from storage. * - * @param {Array} keys The keys to be removed. - * @param {function()} opt_callback Optional function to invoke when the - * remove is complete. You don't have to wait for the set to complete in - * order to read the value, since the local cache is updated synchronously. + * @param {!Array} keys The keys to be removed. + * @override */ -lib.Storage.Local.prototype.removeItems = function(ary, opt_callback) { - for (var i = 0; i < ary.length; i++) { - this.storage_.removeItem(ary[i]); +lib.Storage.Local.prototype.removeItems = async function(keys) { + for (let i = 0; i < keys.length; i++) { + this.storage_.removeItem(keys[i]); } - if (opt_callback) - setTimeout(opt_callback, 0); + // Force deferment for the standard API. + await 0; }; // SOURCE FILE: libdot/js/lib_storage_memory.js // Copyright (c) 2012 The Chromium OS Authors. All rights reserved. @@ -3308,6 +3962,9 @@ lib.Storage.Local.prototype.removeItems = function(ary, opt_callback) { /** * In-memory storage class with an async interface that is interchangeable with * other lib.Storage.* implementations. + * + * @constructor + * @implements {lib.Storage} */ lib.Storage.Memory = function() { this.observers_ = []; @@ -3317,8 +3974,9 @@ lib.Storage.Memory = function() { /** * Register a function to observe storage changes. * - * @param {function(map)} callback The function to invoke when the storage + * @param {function(!Object)} callback The function to invoke when the storage * changes. + * @override */ lib.Storage.Memory.prototype.addObserver = function(callback) { this.observers_.push(callback); @@ -3327,85 +3985,75 @@ lib.Storage.Memory.prototype.addObserver = function(callback) { /** * Unregister a change observer. * - * @param {function} observer A previously registered callback. + * @param {function(!Object)} callback A previously registered callback. + * @override */ lib.Storage.Memory.prototype.removeObserver = function(callback) { - var i = this.observers_.indexOf(callback); - if (i != -1) + const i = this.observers_.indexOf(callback); + if (i != -1) { this.observers_.splice(i, 1); + } }; /** - * Delete everything in this storage. + * Update the internal storage state and generate change events for it. * - * @param {function(map)} callback The function to invoke when the delete - * has completed. + * @param {!Object} newStorage */ -lib.Storage.Memory.prototype.clear = function(opt_callback) { - var e = {}; - for (var key in this.storage_) { - e[key] = {oldValue: this.storage_[key], newValue: (void 0)}; - } +lib.Storage.Memory.prototype.update_ = async function(newStorage) { + const changes = lib.Storage.generateStorageChanges(this.storage_, newStorage); + this.storage_ = newStorage; - this.storage_ = {}; + // Force deferment for the standard API. + await 0; - setTimeout(function() { - for (var i = 0; i < this.observers_.length; i++) { - this.observers_[i](e); - } - }.bind(this), 0); + // Don't bother notifying if there are no changes. + if (Object.keys(changes).length) { + this.observers_.forEach((o) => o(changes)); + } +}; - if (opt_callback) - setTimeout(opt_callback, 0); +/** + * Delete everything in this storage. + * + * @override + */ +lib.Storage.Memory.prototype.clear = async function() { + return this.update_({}); }; /** * Return the current value of a storage item. * * @param {string} key The key to look up. - * @param {function(value) callback The function to invoke when the value has - * been retrieved. + * @override */ -lib.Storage.Memory.prototype.getItem = function(key, callback) { - var value = this.storage_[key]; - - if (typeof value == 'string') { - try { - value = JSON.parse(value); - } catch (e) { - // If we can't parse the value, just return it unparsed. - } - } - - setTimeout(callback.bind(null, value), 0); +lib.Storage.Memory.prototype.getItem = async function(key) { + return this.getItems([key]).then((items) => items[key]); }; /** * Fetch the values of multiple storage items. * - * @param {Array} keys The keys to look up. - * @param {function(map) callback The function to invoke when the values have - * been retrieved. + * @param {?Array} keys The keys to look up. Pass null for all keys. + * @override */ -lib.Storage.Memory.prototype.getItems = function(keys, callback) { - var rv = {}; +lib.Storage.Memory.prototype.getItems = async function(keys) { + const rv = {}; + if (!keys) { + keys = Object.keys(this.storage_); + } - for (var i = keys.length - 1; i >= 0; i--) { - var key = keys[i]; - var value = this.storage_[key]; - if (typeof value == 'string') { - try { - rv[key] = JSON.parse(value); - } catch (e) { - // If we can't parse the value, just return it unparsed. - rv[key] = value; - } - } else { - keys.splice(i, 1); + keys.forEach((key) => { + if (this.storage_.hasOwnProperty(key)) { + rv[key] = this.storage_[key]; } - } + }); - setTimeout(callback.bind(null, rv), 0); + // Force deferment for the standard API. + await 0; + + return rv; }; /** @@ -3414,276 +4062,310 @@ lib.Storage.Memory.prototype.getItems = function(keys, callback) { * @param {string} key The key for the value to be stored. * @param {*} value The value to be stored. Anything that can be serialized * with JSON is acceptable. - * @param {function()} opt_callback Optional function to invoke when the - * set is complete. You don't have to wait for the set to complete in order - * to read the value, since the local cache is updated synchronously. + * @override */ -lib.Storage.Memory.prototype.setItem = function(key, value, opt_callback) { - var oldValue = this.storage_[key]; - this.storage_[key] = JSON.stringify(value); - - var e = {}; - e[key] = {oldValue: oldValue, newValue: value}; - - setTimeout(function() { - for (var i = 0; i < this.observers_.length; i++) { - this.observers_[i](e); - } - }.bind(this), 0); - - if (opt_callback) - setTimeout(opt_callback, 0); +lib.Storage.Memory.prototype.setItem = async function(key, value) { + return this.setItems({[key]: value}); }; /** * Set multiple values in storage. * - * @param {Object} map A map of key/values to set in storage. - * @param {function()} opt_callback Optional function to invoke when the - * set is complete. You don't have to wait for the set to complete in order - * to read the value, since the local cache is updated synchronously. + * @param {!Object} obj A map of key/values to set in storage. + * @override */ -lib.Storage.Memory.prototype.setItems = function(obj, opt_callback) { - var e = {}; - - for (var key in obj) { - e[key] = {oldValue: this.storage_[key], newValue: obj[key]}; - this.storage_[key] = JSON.stringify(obj[key]); +lib.Storage.Memory.prototype.setItems = async function(obj) { + const newStorage = Object.assign({}, this.storage_); + for (const key in obj) { + // Normalize through JSON to mimic Local/Chrome backends. + newStorage[key] = JSON.parse(JSON.stringify(obj[key])); } - - setTimeout(function() { - for (var i = 0; i < this.observers_.length; i++) { - this.observers_[i](e); - } - }.bind(this)); - - if (opt_callback) - setTimeout(opt_callback, 0); + return this.update_(newStorage); }; /** * Remove an item from storage. * * @param {string} key The key to be removed. - * @param {function()} opt_callback Optional function to invoke when the - * remove is complete. You don't have to wait for the set to complete in - * order to read the value, since the local cache is updated synchronously. + * @override */ -lib.Storage.Memory.prototype.removeItem = function(key, opt_callback) { - delete this.storage_[key]; - - if (opt_callback) - setTimeout(opt_callback, 0); +lib.Storage.Memory.prototype.removeItem = async function(key) { + return this.removeItems([key]); }; /** * Remove multiple items from storage. * - * @param {Array} keys The keys to be removed. - * @param {function()} opt_callback Optional function to invoke when the - * remove is complete. You don't have to wait for the set to complete in - * order to read the value, since the local cache is updated synchronously. + * @param {!Array} keys The keys to be removed. + * @override */ -lib.Storage.Memory.prototype.removeItems = function(ary, opt_callback) { - for (var i = 0; i < ary.length; i++) { - delete this.storage_[ary[i]]; - } - - if (opt_callback) - setTimeout(opt_callback, 0); +lib.Storage.Memory.prototype.removeItems = async function(keys) { + const newStorage = Object.assign({}, this.storage_); + keys.forEach((key) => delete newStorage[key]); + return this.update_(newStorage); }; -// SOURCE FILE: libdot/third_party/fast-text-encoding/text.js -/* - * Copyright 2017 Sam Thorogood. 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 - * - * https://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. - */ +// SOURCE FILE: libdot/js/lib_storage_terminal_private.js +// Copyright 2019 The Chromium OS 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 Polyfill for TextEncoder and TextDecoder. + * Storage implementation using chrome.settingsPrivate. * - * You probably want `text.min.js`, and not this file directly. - */ - -(function(scope) { -'use strict'; - -// fail early -if (scope['TextEncoder'] && scope['TextDecoder']) { - return false; -} - -/** + * @param {{ + * getSettings: function(function(?Object)), + * setSettings: function(!Object, function()), + * onSettingsChanged: !ChromeEvent, + * }=} storage * @constructor - * @param {string=} utfLabel + * @implements {lib.Storage} */ -function FastTextEncoder(utfLabel='utf-8') { - if (utfLabel !== 'utf-8') { - throw new RangeError( - `Failed to construct 'TextEncoder': The encoding label provided ('${utfLabel}') is invalid.`); - } -} +lib.Storage.TerminalPrivate = function(storage = chrome.terminalPrivate) { + /** + * @const + * @private + */ + this.observers_ = []; -Object.defineProperty(FastTextEncoder.prototype, 'encoding', {value: 'utf-8'}); + /** + * Local cache of terminalPrivate.getSettings. + * + * @private {!Object} + */ + this.prefValue_ = {}; -/** - * @param {string} string - * @param {{stream: boolean}=} options - * @return {!Uint8Array} - */ -FastTextEncoder.prototype.encode = function(string, options={stream: false}) { - if (options.stream) { - throw new Error(`Failed to encode: the 'stream' option is unsupported.`); - } + /** + * We do async writes to terminalPrivate.setSettings to allow multiple sync + * writes to be batched. This array holds the list of pending resolve calls + * that we'll invoke when the current write finishes. + * + * @private {!Array} + */ + this.prefValueWriteToResolve_ = []; - let pos = 0; - const len = string.length; - const out = []; - - let at = 0; // output position - let tlen = Math.max(32, len + (len >> 1) + 7); // 1.5x size - let target = new Uint8Array((tlen >> 3) << 3); // ... but at 8 byte offset - - while (pos < len) { - let value = string.charCodeAt(pos++); - if (value >= 0xd800 && value <= 0xdbff) { - // high surrogate - if (pos < len) { - const extra = string.charCodeAt(pos); - if ((extra & 0xfc00) === 0xdc00) { - ++pos; - value = ((value & 0x3ff) << 10) + (extra & 0x3ff) + 0x10000; - } - } - if (value >= 0xd800 && value <= 0xdbff) { - continue; // drop lone surrogate - } - } + /** @type {boolean} */ + this.prefsLoaded_ = false; - // expand the buffer if we couldn't write 4 bytes - if (at + 4 > target.length) { - tlen += 8; // minimum extra - tlen *= (1.0 + (pos / string.length) * 2); // take 2x the remaining - tlen = (tlen >> 3) << 3; // 8 byte offset + /** @const */ + this.storage_ = storage; - const update = new Uint8Array(tlen); - update.set(target); - target = update; - } + this.storage_.onSettingsChanged.addListener( + this.onSettingsChanged_.bind(this)); +}; - if ((value & 0xffffff80) === 0) { // 1-byte - target[at++] = value; // ASCII - continue; - } else if ((value & 0xfffff800) === 0) { // 2-byte - target[at++] = ((value >> 6) & 0x1f) | 0xc0; - } else if ((value & 0xffff0000) === 0) { // 3-byte - target[at++] = ((value >> 12) & 0x0f) | 0xe0; - target[at++] = ((value >> 6) & 0x3f) | 0x80; - } else if ((value & 0xffe00000) === 0) { // 4-byte - target[at++] = ((value >> 18) & 0x07) | 0xf0; - target[at++] = ((value >> 12) & 0x3f) | 0x80; - target[at++] = ((value >> 6) & 0x3f) | 0x80; - } else { - // FIXME: do we care - continue; +/** + * Load the settings into our local cache. + * + * @return {!Promise} Resolves when settings have been loaded. + */ +lib.Storage.TerminalPrivate.prototype.initCache_ = function() { + return new Promise((resolve) => { + // NB: This doesn't return Promise.resolve so we're guaranteed to have the + // initCache_ call always return deferred execution. + if (this.prefsLoaded_) { + resolve(); + return; } - target[at++] = (value & 0x3f) | 0x80; - } - - return target.slice(0, at); -} + this.storage_.getSettings((settings) => { + const err = lib.f.lastError(); + if (err) { + console.error(err); + } else { + this.prefValue_ = lib.notNull(settings); + } + this.prefsLoaded_ = true; + resolve(); + }); + }); +}; /** - * @constructor - * @param {string=} utfLabel - * @param {{fatal: boolean}=} options + * Called when settings change. + * + * @param {!Object} settings + * @private */ -function FastTextDecoder(utfLabel='utf-8', options={fatal: false}) { - if (utfLabel !== 'utf-8') { - throw new RangeError( - `Failed to construct 'TextDecoder': The encoding label provided ('${utfLabel}') is invalid.`); - } - if (options.fatal) { - throw new Error(`Failed to construct 'TextDecoder': the 'fatal' option is unsupported.`); - } -} - -Object.defineProperty(FastTextDecoder.prototype, 'encoding', {value: 'utf-8'}); +lib.Storage.TerminalPrivate.prototype.onSettingsChanged_ = function(settings) { + // Check what is deleted. + const changes = lib.Storage.generateStorageChanges(this.prefValue_, settings); + this.prefValue_ = settings; -Object.defineProperty(FastTextDecoder.prototype, 'fatal', {value: false}); - -Object.defineProperty(FastTextDecoder.prototype, 'ignoreBOM', {value: false}); + // Don't bother notifying if there are no changes. + if (Object.keys(changes).length) { + setTimeout(() => { + this.observers_.forEach((o) => o(changes)); + }); + } +}; /** - * @param {(!ArrayBuffer|!ArrayBufferView)} buffer - * @param {{stream: boolean}=} options + * Set pref then run callback. Writes are done async to allow multiple + * concurrent calls to this function to be batched into a single write. + * + * @return {!Promise} Resolves once the pref is set. + * @private */ -FastTextDecoder.prototype.decode = function(buffer, options={stream: false}) { - if (options['stream']) { - throw new Error(`Failed to decode: the 'stream' option is unsupported.`); - } +lib.Storage.TerminalPrivate.prototype.setPref_ = function() { + lib.assert(this.prefsLoaded_); - const bytes = new Uint8Array(buffer); - let pos = 0; - const len = bytes.length; - const out = []; - - while (pos < len) { - const byte1 = bytes[pos++]; - if (byte1 === 0) { - break; // NULL - } - - if ((byte1 & 0x80) === 0) { // 1-byte - out.push(byte1); - } else if ((byte1 & 0xe0) === 0xc0) { // 2-byte - const byte2 = bytes[pos++] & 0x3f; - out.push(((byte1 & 0x1f) << 6) | byte2); - } else if ((byte1 & 0xf0) === 0xe0) { - const byte2 = bytes[pos++] & 0x3f; - const byte3 = bytes[pos++] & 0x3f; - out.push(((byte1 & 0x1f) << 12) | (byte2 << 6) | byte3); - } else if ((byte1 & 0xf8) === 0xf0) { - const byte2 = bytes[pos++] & 0x3f; - const byte3 = bytes[pos++] & 0x3f; - const byte4 = bytes[pos++] & 0x3f; - - // this can be > 0xffff, so possibly generate surrogates - let codepoint = ((byte1 & 0x07) << 0x12) | (byte2 << 0x0c) | (byte3 << 0x06) | byte4; - if (codepoint > 0xffff) { - // codepoint &= ~0x10000; - codepoint -= 0x10000; - out.push((codepoint >>> 10) & 0x3ff | 0xd800) - codepoint = 0xdc00 | codepoint & 0x3ff; - } - out.push(codepoint); - } else { - // FIXME: we're ignoring this + return new Promise((resolve) => { + this.prefValueWriteToResolve_.push(resolve); + if (this.prefValueWriteToResolve_.length > 1) { + return; } + + // Force deferment to help coalesce. + setTimeout(() => { + this.storage_.setSettings(this.prefValue_, () => { + const err = lib.f.lastError(); + if (err) { + console.error(err); + } + // Resolve all the pending promises so their callbacks will be invoked + // once this function returns. + this.prefValueWriteToResolve_.forEach((r) => r()); + this.prefValueWriteToResolve_ = []; + }); + }); + }); +}; + +/** + * Register a function to observe storage changes. + * + * @param {function(!Object)} callback The function to invoke when the storage + * changes. + * @override + */ +lib.Storage.TerminalPrivate.prototype.addObserver = function(callback) { + this.observers_.push(callback); +}; + +/** + * Unregister a change observer. + * + * @param {function(!Object)} callback A previously registered callback. + * @override + */ +lib.Storage.TerminalPrivate.prototype.removeObserver = function(callback) { + const i = this.observers_.indexOf(callback); + if (i !== -1) { + this.observers_.splice(i, 1); } +}; - return String.fromCharCode.apply(null, out); -} +/** + * Update the internal storage state and generate change events for it. + * + * @param {!Object} newStorage + */ +lib.Storage.TerminalPrivate.prototype.update_ = async function(newStorage) { + const changes = lib.Storage.generateStorageChanges( + this.prefValue_, newStorage); + this.prefValue_ = newStorage; + + await this.setPref_(); + + // Don't bother notifying if there are no changes. + if (Object.keys(changes).length) { + this.observers_.forEach((o) => o(changes)); + } +}; + +/** + * Delete everything in this storage. + * + * @override + */ +lib.Storage.TerminalPrivate.prototype.clear = async function() { + await this.initCache_(); + return this.update_({}); +}; + +/** + * Return the current value of a storage item. + * + * @param {string} key The key to look up. + * @override + */ +lib.Storage.TerminalPrivate.prototype.getItem = async function(key) { + await this.initCache_(); + return this.prefValue_[key]; +}; + +/** + * Fetch the values of multiple storage items. + * + * @param {?Array} keys The keys to look up. Pass null for all keys. + * @override + */ +lib.Storage.TerminalPrivate.prototype.getItems = async function(keys) { + await this.initCache_(); + + const rv = {}; + if (!keys) { + keys = Object.keys(this.prefValue_); + } + + for (const key of keys) { + if (this.prefValue_.hasOwnProperty(key)) { + rv[key] = this.prefValue_[key]; + } + } + + return rv; +}; + +/** + * Set a value in storage. + * + * @param {string} key The key for the value to be stored. + * @param {*} value The value to be stored. Anything that can be serialized + * with JSON is acceptable. + * @override + */ +lib.Storage.TerminalPrivate.prototype.setItem = async function(key, value) { + return this.setItems({[key]: value}); +}; + +/** + * Set multiple values in storage. + * + * @param {!Object} obj A map of key/values to set in storage. + * @override + */ +lib.Storage.TerminalPrivate.prototype.setItems = async function(obj) { + await this.initCache_(); + return this.update_(Object.assign({}, this.prefValue_, obj)); +}; -scope['TextEncoder'] = FastTextEncoder; -scope['TextDecoder'] = FastTextDecoder; +/** + * Remove an item from storage. + * + * @param {string} key The key to be removed. + * @override + */ +lib.Storage.TerminalPrivate.prototype.removeItem = async function(key) { + return this.removeItems([key]); +}; -}(typeof window !== 'undefined' ? window : (typeof global !== 'undefined' ? global : this))); +/** + * Remove multiple items from storage. + * + * @param {!Array} keys The keys to be removed. + * @override + */ +lib.Storage.TerminalPrivate.prototype.removeItems = async function(keys) { + await this.initCache_(); + const newStorage = Object.assign({}, this.prefValue_); + keys.forEach((key) => delete newStorage[key]); + return this.update_(newStorage); +}; // SOURCE FILE: libdot/third_party/intl-segmenter/intl-segmenter.js // Rough polyfill for Intl.Segmenter proposal // -// https://github.com/tc39/proposal-intl-segmenter/blob/master/README.md +// https://github.com/tc39/proposal-intl-segmenter/blob/HEAD/README.md // // Caveats and Limitations // * granularity: 'line': 'strictness' option is not supported (ignored) @@ -3694,30 +4376,31 @@ scope['TextDecoder'] = FastTextDecoder; // * granularity: 'sentence' does not understand decimals (function(global) { - if ('Intl' in global && 'Segmenter' in global.Intl) + if ('Intl' in global && 'Segmenter' in global.Intl) { return; + } global.Intl = global.Intl || {}; - const GRANULARITIES = ['grapheme', 'word','sentence', 'line']; + const GRANULARITIES = ['grapheme', 'word', 'sentence', 'line']; // TODO: Implement https://www.unicode.org/reports/tr29/ const RULES = { grapheme: { - grapheme: /^(.|\n)/ + grapheme: /^(.|\n)/, }, word: { letter: /^[a-z](?:'?[a-z])*/i, - number: /^\d+([,.]\d+)*/ + number: /^\d+([,.]\d+)*/, }, sentence: { terminator: /^[^.?!\r\n]+[.?!]+[\r\n]?/, - separator: /^[^.?!\r\n]+[\r\n]?/ + separator: /^[^.?!\r\n]+[\r\n]?/, }, line: { hard: /^\S*[\r\n]/, - soft: /^\S*\s*/ - } + soft: /^\S*\s*/, + }, }; // Work around bug in v8BreakIterator where ICU's UWordBreak enum is @@ -3732,7 +4415,7 @@ scope['TextDecoder'] = FastTextDecoder; letter: 200, // UBRK_WORD_LETTER kana: 300, // UBRK_WORD_KANA ideo: 400, // UBRK_WORD_IDEO - unknown: -1 + unknown: -1, }[value] || 0; @@ -3745,13 +4428,13 @@ scope['TextDecoder'] = FastTextDecoder; // Map ULineBreakTag rule status to string. return { 0: 'terminator', - 100: 'separator' + 100: 'separator', }[ruleStatus] || value; case 'line': // Map ULineBreakTag rule status to string. return { 0: 'soft', - 100: 'hard' + 100: 'hard', }[ruleStatus] || value; default: return value; @@ -3761,8 +4444,9 @@ scope['TextDecoder'] = FastTextDecoder; function segment(locale, granularity, string) { const breaks = []; if ('v8BreakIterator' in global.Intl) { - if (granularity === 'grapheme') + if (granularity === 'grapheme') { granularity = 'character'; + } const vbi = new global.Intl.v8BreakIterator(locale, {type: granularity}); vbi.adoptText(string); let last = 0; @@ -3771,7 +4455,7 @@ scope['TextDecoder'] = FastTextDecoder; breaks.push({ pos: vbi.current(), segment: string.slice(last, pos), - breakType: fixBreakType(vbi.breakType(), granularity) + breakType: fixBreakType(vbi.breakType(), granularity), }); last = pos; pos = vbi.next(); @@ -3781,7 +4465,7 @@ scope['TextDecoder'] = FastTextDecoder; let pos = 0; while (pos < string.length) { let found = false; - for (let rule of Object.keys(rules)) { + for (const rule of Object.keys(rules)) { const re = rules[rule]; const m = string.slice(pos).match(re); if (m) { @@ -3789,7 +4473,7 @@ scope['TextDecoder'] = FastTextDecoder; breaks.push({ pos: pos, segment: m[0], - breakType: granularity === 'grapheme' ? undefined : rule + breakType: granularity === 'grapheme' ? undefined : rule, }); found = true; break; @@ -3799,12 +4483,11 @@ scope['TextDecoder'] = FastTextDecoder; breaks.push({ pos: pos + 1, segment: string.slice(pos, ++pos), - breakType: 'none' + breakType: 'none', }); } } } - breaks.initial = 0; return breaks; } @@ -3815,11 +4498,14 @@ scope['TextDecoder'] = FastTextDecoder; this._breaks = breaks; } - [Symbol.iterator]() { return this; } + [Symbol.iterator]() { + return this; + } next() { - if (this._cur < this._breaks.length) + if (this._cur < this._breaks.length) { ++this._cur; + } if (this._cur >= this._breaks.length) { this._type = undefined; @@ -3831,23 +4517,25 @@ scope['TextDecoder'] = FastTextDecoder; done: false, value: { segment: this._breaks[this._cur].segment, - breakType: this._breaks[this._cur].breakType - } + breakType: this._breaks[this._cur].breakType, + }, }; } following(index = undefined) { - if (!this._breaks.length) + if (!this._breaks.length) { return true; + } if (index === undefined) { - if (this._cur < this._breaks.length) + if (this._cur < this._breaks.length) { ++this._cur; + } } else { // TODO: binary search for (this._cur = 0; this._cur < this._breaks.length && this._breaks[this._cur].pos < index; - ++this._cur) {} + ++this._cur) { /* TODO */ } } this._type = this._cur < this._breaks.length @@ -3856,19 +4544,22 @@ scope['TextDecoder'] = FastTextDecoder; } preceding(index = undefined) { - if (!this._breaks.length) + if (!this._breaks.length) { return true; + } if (index === undefined) { - if (this._cur >= this._breaks.length) + if (this._cur >= this._breaks.length) { --this._cur; - if (this._cur >= 0) + } + if (this._cur >= 0) { --this._cur; + } } else { // TODO: binary search for (this._cur = this._breaks.length - 1; this._cur >= 0 && this._breaks[this._cur].pos >= index; - --this._cur) {} + --this._cur) { /* TODO */ } } this._type = @@ -3878,10 +4569,12 @@ scope['TextDecoder'] = FastTextDecoder; } get position() { - if (this._cur < 0 || !this._breaks.length) - return this._breaks.initial; - if (this._cur >= this._breaks.length) + if (this._cur < 0 || !this._breaks.length) { + return 0; + } + if (this._cur >= this._breaks.length) { return this._breaks[this._breaks.length - 1].pos; + } return this._breaks[this._cur].pos; } @@ -3891,12 +4584,11 @@ scope['TextDecoder'] = FastTextDecoder; } global.Intl.Segmenter = class Segmenter { - constructor(locale, options) { + constructor(locale, {localeMatcher, granularity = 'grapheme'} = {}) { this._locale = Array.isArray(locale) - ? locale.map(s => String(s)) : String(locale || navigator.language); - options = Object.assign({granularity: 'grapheme'}, options); - this._granularity = GRANULARITIES.includes(options.granularity) - ? options.granularity : 'grapheme'; + ? locale.map((s) => String(s)) : String(locale || navigator.language); + this._granularity = GRANULARITIES.includes(granularity) + ? granularity : 'grapheme'; } segment(string) { @@ -3904,13 +4596,16 @@ scope['TextDecoder'] = FastTextDecoder; string, segment(this._locale, this._granularity, string)); } }; -}(typeof window !== 'undefined' ? window : (typeof global !== 'undefined' ? global : this))); +}(typeof window !== 'undefined' ? + window : + (typeof global !== 'undefined' ? global : this))); // SOURCE FILE: libdot/third_party/wcwidth/lib_wc.js // Copyright (c) 2014 The Chromium OS Authors. All rights reserved. // Use of lib.wc source code is governed by a BSD-style license that can be // found in the LICENSE file. /** + * @fileoverview * This JavaScript library is ported from the wcwidth.js module of node.js. * The original implementation can be found at: * https://npmjs.org/package/wcwidth.js @@ -4022,259 +4717,268 @@ lib.wc.cjkAmbiguousWidth = 2; // Sorted list of non-overlapping intervals of non-spacing characters // generated by the `./ranges.py` helper. lib.wc.combining = [ - [0x00ad, 0x00ad], [0x0300, 0x036f], [0x0483, 0x0489], - [0x0591, 0x05bd], [0x05bf, 0x05bf], [0x05c1, 0x05c2], - [0x05c4, 0x05c5], [0x05c7, 0x05c7], [0x0610, 0x061a], - [0x061c, 0x061c], [0x064b, 0x065f], [0x0670, 0x0670], - [0x06d6, 0x06dc], [0x06df, 0x06e4], [0x06e7, 0x06e8], - [0x06ea, 0x06ed], [0x0711, 0x0711], [0x0730, 0x074a], - [0x07a6, 0x07b0], [0x07eb, 0x07f3], [0x07fd, 0x07fd], - [0x0816, 0x0819], [0x081b, 0x0823], [0x0825, 0x0827], - [0x0829, 0x082d], [0x0859, 0x085b], [0x08d3, 0x08e1], - [0x08e3, 0x0902], [0x093a, 0x093a], [0x093c, 0x093c], - [0x0941, 0x0948], [0x094d, 0x094d], [0x0951, 0x0957], - [0x0962, 0x0963], [0x0981, 0x0981], [0x09bc, 0x09bc], - [0x09c1, 0x09c4], [0x09cd, 0x09cd], [0x09e2, 0x09e3], - [0x09fe, 0x09fe], [0x0a01, 0x0a02], [0x0a3c, 0x0a3c], - [0x0a41, 0x0a42], [0x0a47, 0x0a48], [0x0a4b, 0x0a4d], - [0x0a51, 0x0a51], [0x0a70, 0x0a71], [0x0a75, 0x0a75], - [0x0a81, 0x0a82], [0x0abc, 0x0abc], [0x0ac1, 0x0ac5], - [0x0ac7, 0x0ac8], [0x0acd, 0x0acd], [0x0ae2, 0x0ae3], - [0x0afa, 0x0aff], [0x0b01, 0x0b01], [0x0b3c, 0x0b3c], - [0x0b3f, 0x0b3f], [0x0b41, 0x0b44], [0x0b4d, 0x0b4d], - [0x0b56, 0x0b56], [0x0b62, 0x0b63], [0x0b82, 0x0b82], - [0x0bc0, 0x0bc0], [0x0bcd, 0x0bcd], [0x0c00, 0x0c00], - [0x0c04, 0x0c04], [0x0c3e, 0x0c40], [0x0c46, 0x0c48], - [0x0c4a, 0x0c4d], [0x0c55, 0x0c56], [0x0c62, 0x0c63], - [0x0c81, 0x0c81], [0x0cbc, 0x0cbc], [0x0cbf, 0x0cbf], - [0x0cc6, 0x0cc6], [0x0ccc, 0x0ccd], [0x0ce2, 0x0ce3], - [0x0d00, 0x0d01], [0x0d3b, 0x0d3c], [0x0d41, 0x0d44], - [0x0d4d, 0x0d4d], [0x0d62, 0x0d63], [0x0dca, 0x0dca], - [0x0dd2, 0x0dd4], [0x0dd6, 0x0dd6], [0x0e31, 0x0e31], - [0x0e34, 0x0e3a], [0x0e47, 0x0e4e], [0x0eb1, 0x0eb1], - [0x0eb4, 0x0ebc], [0x0ec8, 0x0ecd], [0x0f18, 0x0f19], - [0x0f35, 0x0f35], [0x0f37, 0x0f37], [0x0f39, 0x0f39], - [0x0f71, 0x0f7e], [0x0f80, 0x0f84], [0x0f86, 0x0f87], - [0x0f8d, 0x0f97], [0x0f99, 0x0fbc], [0x0fc6, 0x0fc6], - [0x102d, 0x1030], [0x1032, 0x1037], [0x1039, 0x103a], - [0x103d, 0x103e], [0x1058, 0x1059], [0x105e, 0x1060], - [0x1071, 0x1074], [0x1082, 0x1082], [0x1085, 0x1086], - [0x108d, 0x108d], [0x109d, 0x109d], [0x1160, 0x11ff], - [0x135d, 0x135f], [0x1712, 0x1714], [0x1732, 0x1734], - [0x1752, 0x1753], [0x1772, 0x1773], [0x17b4, 0x17b5], - [0x17b7, 0x17bd], [0x17c6, 0x17c6], [0x17c9, 0x17d3], - [0x17dd, 0x17dd], [0x180b, 0x180e], [0x1885, 0x1886], - [0x18a9, 0x18a9], [0x1920, 0x1922], [0x1927, 0x1928], - [0x1932, 0x1932], [0x1939, 0x193b], [0x1a17, 0x1a18], - [0x1a1b, 0x1a1b], [0x1a56, 0x1a56], [0x1a58, 0x1a5e], - [0x1a60, 0x1a60], [0x1a62, 0x1a62], [0x1a65, 0x1a6c], - [0x1a73, 0x1a7c], [0x1a7f, 0x1a7f], [0x1ab0, 0x1abe], - [0x1b00, 0x1b03], [0x1b34, 0x1b34], [0x1b36, 0x1b3a], - [0x1b3c, 0x1b3c], [0x1b42, 0x1b42], [0x1b6b, 0x1b73], - [0x1b80, 0x1b81], [0x1ba2, 0x1ba5], [0x1ba8, 0x1ba9], - [0x1bab, 0x1bad], [0x1be6, 0x1be6], [0x1be8, 0x1be9], - [0x1bed, 0x1bed], [0x1bef, 0x1bf1], [0x1c2c, 0x1c33], - [0x1c36, 0x1c37], [0x1cd0, 0x1cd2], [0x1cd4, 0x1ce0], - [0x1ce2, 0x1ce8], [0x1ced, 0x1ced], [0x1cf4, 0x1cf4], - [0x1cf8, 0x1cf9], [0x1dc0, 0x1df9], [0x1dfb, 0x1dff], - [0x200b, 0x200f], [0x202a, 0x202e], [0x2060, 0x2064], - [0x2066, 0x206f], [0x20d0, 0x20f0], [0x2cef, 0x2cf1], - [0x2d7f, 0x2d7f], [0x2de0, 0x2dff], [0x302a, 0x302d], - [0x3099, 0x309a], [0xa66f, 0xa672], [0xa674, 0xa67d], - [0xa69e, 0xa69f], [0xa6f0, 0xa6f1], [0xa802, 0xa802], - [0xa806, 0xa806], [0xa80b, 0xa80b], [0xa825, 0xa826], - [0xa8c4, 0xa8c5], [0xa8e0, 0xa8f1], [0xa8ff, 0xa8ff], - [0xa926, 0xa92d], [0xa947, 0xa951], [0xa980, 0xa982], - [0xa9b3, 0xa9b3], [0xa9b6, 0xa9b9], [0xa9bc, 0xa9bd], - [0xa9e5, 0xa9e5], [0xaa29, 0xaa2e], [0xaa31, 0xaa32], - [0xaa35, 0xaa36], [0xaa43, 0xaa43], [0xaa4c, 0xaa4c], - [0xaa7c, 0xaa7c], [0xaab0, 0xaab0], [0xaab2, 0xaab4], - [0xaab7, 0xaab8], [0xaabe, 0xaabf], [0xaac1, 0xaac1], - [0xaaec, 0xaaed], [0xaaf6, 0xaaf6], [0xabe5, 0xabe5], - [0xabe8, 0xabe8], [0xabed, 0xabed], [0xfb1e, 0xfb1e], - [0xfe00, 0xfe0f], [0xfe20, 0xfe2f], [0xfeff, 0xfeff], - [0xfff9, 0xfffb], [0x101fd, 0x101fd], [0x102e0, 0x102e0], - [0x10376, 0x1037a], [0x10a01, 0x10a03], [0x10a05, 0x10a06], - [0x10a0c, 0x10a0f], [0x10a38, 0x10a3a], [0x10a3f, 0x10a3f], - [0x10ae5, 0x10ae6], [0x10d24, 0x10d27], [0x10f46, 0x10f50], - [0x11001, 0x11001], [0x11038, 0x11046], [0x1107f, 0x11081], - [0x110b3, 0x110b6], [0x110b9, 0x110ba], [0x11100, 0x11102], - [0x11127, 0x1112b], [0x1112d, 0x11134], [0x11173, 0x11173], - [0x11180, 0x11181], [0x111b6, 0x111be], [0x111c9, 0x111cc], - [0x1122f, 0x11231], [0x11234, 0x11234], [0x11236, 0x11237], - [0x1123e, 0x1123e], [0x112df, 0x112df], [0x112e3, 0x112ea], - [0x11300, 0x11301], [0x1133b, 0x1133c], [0x11340, 0x11340], - [0x11366, 0x1136c], [0x11370, 0x11374], [0x11438, 0x1143f], - [0x11442, 0x11444], [0x11446, 0x11446], [0x1145e, 0x1145e], - [0x114b3, 0x114b8], [0x114ba, 0x114ba], [0x114bf, 0x114c0], - [0x114c2, 0x114c3], [0x115b2, 0x115b5], [0x115bc, 0x115bd], - [0x115bf, 0x115c0], [0x115dc, 0x115dd], [0x11633, 0x1163a], - [0x1163d, 0x1163d], [0x1163f, 0x11640], [0x116ab, 0x116ab], - [0x116ad, 0x116ad], [0x116b0, 0x116b5], [0x116b7, 0x116b7], - [0x1171d, 0x1171f], [0x11722, 0x11725], [0x11727, 0x1172b], - [0x1182f, 0x11837], [0x11839, 0x1183a], [0x119d4, 0x119d7], - [0x119da, 0x119db], [0x119e0, 0x119e0], [0x11a01, 0x11a0a], - [0x11a33, 0x11a38], [0x11a3b, 0x11a3e], [0x11a47, 0x11a47], - [0x11a51, 0x11a56], [0x11a59, 0x11a5b], [0x11a8a, 0x11a96], - [0x11a98, 0x11a99], [0x11c30, 0x11c36], [0x11c38, 0x11c3d], - [0x11c3f, 0x11c3f], [0x11c92, 0x11ca7], [0x11caa, 0x11cb0], - [0x11cb2, 0x11cb3], [0x11cb5, 0x11cb6], [0x11d31, 0x11d36], - [0x11d3a, 0x11d3a], [0x11d3c, 0x11d3d], [0x11d3f, 0x11d45], - [0x11d47, 0x11d47], [0x11d90, 0x11d91], [0x11d95, 0x11d95], - [0x11d97, 0x11d97], [0x11ef3, 0x11ef4], [0x13430, 0x13438], - [0x16af0, 0x16af4], [0x16b30, 0x16b36], [0x16f4f, 0x16f4f], - [0x16f8f, 0x16f92], [0x1bc9d, 0x1bc9e], [0x1bca0, 0x1bca3], - [0x1d167, 0x1d169], [0x1d173, 0x1d182], [0x1d185, 0x1d18b], - [0x1d1aa, 0x1d1ad], [0x1d242, 0x1d244], [0x1da00, 0x1da36], - [0x1da3b, 0x1da6c], [0x1da75, 0x1da75], [0x1da84, 0x1da84], - [0x1da9b, 0x1da9f], [0x1daa1, 0x1daaf], [0x1e000, 0x1e006], - [0x1e008, 0x1e018], [0x1e01b, 0x1e021], [0x1e023, 0x1e024], - [0x1e026, 0x1e02a], [0x1e130, 0x1e136], [0x1e2ec, 0x1e2ef], - [0x1e8d0, 0x1e8d6], [0x1e944, 0x1e94a], [0xe0001, 0xe0001], - [0xe0020, 0xe007f], [0xe0100, 0xe01ef], + [0x00ad, 0x00ad], [0x0300, 0x036f], [0x0483, 0x0489], + [0x0591, 0x05bd], [0x05bf, 0x05bf], [0x05c1, 0x05c2], + [0x05c4, 0x05c5], [0x05c7, 0x05c7], [0x0610, 0x061a], + [0x061c, 0x061c], [0x064b, 0x065f], [0x0670, 0x0670], + [0x06d6, 0x06dc], [0x06df, 0x06e4], [0x06e7, 0x06e8], + [0x06ea, 0x06ed], [0x0711, 0x0711], [0x0730, 0x074a], + [0x07a6, 0x07b0], [0x07eb, 0x07f3], [0x07fd, 0x07fd], + [0x0816, 0x0819], [0x081b, 0x0823], [0x0825, 0x0827], + [0x0829, 0x082d], [0x0859, 0x085b], [0x08d3, 0x08e1], + [0x08e3, 0x0902], [0x093a, 0x093a], [0x093c, 0x093c], + [0x0941, 0x0948], [0x094d, 0x094d], [0x0951, 0x0957], + [0x0962, 0x0963], [0x0981, 0x0981], [0x09bc, 0x09bc], + [0x09c1, 0x09c4], [0x09cd, 0x09cd], [0x09e2, 0x09e3], + [0x09fe, 0x09fe], [0x0a01, 0x0a02], [0x0a3c, 0x0a3c], + [0x0a41, 0x0a42], [0x0a47, 0x0a48], [0x0a4b, 0x0a4d], + [0x0a51, 0x0a51], [0x0a70, 0x0a71], [0x0a75, 0x0a75], + [0x0a81, 0x0a82], [0x0abc, 0x0abc], [0x0ac1, 0x0ac5], + [0x0ac7, 0x0ac8], [0x0acd, 0x0acd], [0x0ae2, 0x0ae3], + [0x0afa, 0x0aff], [0x0b01, 0x0b01], [0x0b3c, 0x0b3c], + [0x0b3f, 0x0b3f], [0x0b41, 0x0b44], [0x0b4d, 0x0b4d], + [0x0b55, 0x0b56], [0x0b62, 0x0b63], [0x0b82, 0x0b82], + [0x0bc0, 0x0bc0], [0x0bcd, 0x0bcd], [0x0c00, 0x0c00], + [0x0c04, 0x0c04], [0x0c3e, 0x0c40], [0x0c46, 0x0c48], + [0x0c4a, 0x0c4d], [0x0c55, 0x0c56], [0x0c62, 0x0c63], + [0x0c81, 0x0c81], [0x0cbc, 0x0cbc], [0x0cbf, 0x0cbf], + [0x0cc6, 0x0cc6], [0x0ccc, 0x0ccd], [0x0ce2, 0x0ce3], + [0x0d00, 0x0d01], [0x0d3b, 0x0d3c], [0x0d41, 0x0d44], + [0x0d4d, 0x0d4d], [0x0d62, 0x0d63], [0x0d81, 0x0d81], + [0x0dca, 0x0dca], [0x0dd2, 0x0dd4], [0x0dd6, 0x0dd6], + [0x0e31, 0x0e31], [0x0e34, 0x0e3a], [0x0e47, 0x0e4e], + [0x0eb1, 0x0eb1], [0x0eb4, 0x0ebc], [0x0ec8, 0x0ecd], + [0x0f18, 0x0f19], [0x0f35, 0x0f35], [0x0f37, 0x0f37], + [0x0f39, 0x0f39], [0x0f71, 0x0f7e], [0x0f80, 0x0f84], + [0x0f86, 0x0f87], [0x0f8d, 0x0f97], [0x0f99, 0x0fbc], + [0x0fc6, 0x0fc6], [0x102d, 0x1030], [0x1032, 0x1037], + [0x1039, 0x103a], [0x103d, 0x103e], [0x1058, 0x1059], + [0x105e, 0x1060], [0x1071, 0x1074], [0x1082, 0x1082], + [0x1085, 0x1086], [0x108d, 0x108d], [0x109d, 0x109d], + [0x1160, 0x11ff], [0x135d, 0x135f], [0x1712, 0x1714], + [0x1732, 0x1734], [0x1752, 0x1753], [0x1772, 0x1773], + [0x17b4, 0x17b5], [0x17b7, 0x17bd], [0x17c6, 0x17c6], + [0x17c9, 0x17d3], [0x17dd, 0x17dd], [0x180b, 0x180e], + [0x1885, 0x1886], [0x18a9, 0x18a9], [0x1920, 0x1922], + [0x1927, 0x1928], [0x1932, 0x1932], [0x1939, 0x193b], + [0x1a17, 0x1a18], [0x1a1b, 0x1a1b], [0x1a56, 0x1a56], + [0x1a58, 0x1a5e], [0x1a60, 0x1a60], [0x1a62, 0x1a62], + [0x1a65, 0x1a6c], [0x1a73, 0x1a7c], [0x1a7f, 0x1a7f], + [0x1ab0, 0x1ac0], [0x1b00, 0x1b03], [0x1b34, 0x1b34], + [0x1b36, 0x1b3a], [0x1b3c, 0x1b3c], [0x1b42, 0x1b42], + [0x1b6b, 0x1b73], [0x1b80, 0x1b81], [0x1ba2, 0x1ba5], + [0x1ba8, 0x1ba9], [0x1bab, 0x1bad], [0x1be6, 0x1be6], + [0x1be8, 0x1be9], [0x1bed, 0x1bed], [0x1bef, 0x1bf1], + [0x1c2c, 0x1c33], [0x1c36, 0x1c37], [0x1cd0, 0x1cd2], + [0x1cd4, 0x1ce0], [0x1ce2, 0x1ce8], [0x1ced, 0x1ced], + [0x1cf4, 0x1cf4], [0x1cf8, 0x1cf9], [0x1dc0, 0x1df9], + [0x1dfb, 0x1dff], [0x200b, 0x200f], [0x202a, 0x202e], + [0x2060, 0x2064], [0x2066, 0x206f], [0x20d0, 0x20f0], + [0x2cef, 0x2cf1], [0x2d7f, 0x2d7f], [0x2de0, 0x2dff], + [0x302a, 0x302d], [0x3099, 0x309a], [0xa66f, 0xa672], + [0xa674, 0xa67d], [0xa69e, 0xa69f], [0xa6f0, 0xa6f1], + [0xa802, 0xa802], [0xa806, 0xa806], [0xa80b, 0xa80b], + [0xa825, 0xa826], [0xa82c, 0xa82c], [0xa8c4, 0xa8c5], + [0xa8e0, 0xa8f1], [0xa8ff, 0xa8ff], [0xa926, 0xa92d], + [0xa947, 0xa951], [0xa980, 0xa982], [0xa9b3, 0xa9b3], + [0xa9b6, 0xa9b9], [0xa9bc, 0xa9bd], [0xa9e5, 0xa9e5], + [0xaa29, 0xaa2e], [0xaa31, 0xaa32], [0xaa35, 0xaa36], + [0xaa43, 0xaa43], [0xaa4c, 0xaa4c], [0xaa7c, 0xaa7c], + [0xaab0, 0xaab0], [0xaab2, 0xaab4], [0xaab7, 0xaab8], + [0xaabe, 0xaabf], [0xaac1, 0xaac1], [0xaaec, 0xaaed], + [0xaaf6, 0xaaf6], [0xabe5, 0xabe5], [0xabe8, 0xabe8], + [0xabed, 0xabed], [0xfb1e, 0xfb1e], [0xfe00, 0xfe0f], + [0xfe20, 0xfe2f], [0xfeff, 0xfeff], [0xfff9, 0xfffb], + [0x101fd, 0x101fd], [0x102e0, 0x102e0], [0x10376, 0x1037a], + [0x10a01, 0x10a03], [0x10a05, 0x10a06], [0x10a0c, 0x10a0f], + [0x10a38, 0x10a3a], [0x10a3f, 0x10a3f], [0x10ae5, 0x10ae6], + [0x10d24, 0x10d27], [0x10eab, 0x10eac], [0x10f46, 0x10f50], + [0x11001, 0x11001], [0x11038, 0x11046], [0x1107f, 0x11081], + [0x110b3, 0x110b6], [0x110b9, 0x110ba], [0x11100, 0x11102], + [0x11127, 0x1112b], [0x1112d, 0x11134], [0x11173, 0x11173], + [0x11180, 0x11181], [0x111b6, 0x111be], [0x111c9, 0x111cc], + [0x111cf, 0x111cf], [0x1122f, 0x11231], [0x11234, 0x11234], + [0x11236, 0x11237], [0x1123e, 0x1123e], [0x112df, 0x112df], + [0x112e3, 0x112ea], [0x11300, 0x11301], [0x1133b, 0x1133c], + [0x11340, 0x11340], [0x11366, 0x1136c], [0x11370, 0x11374], + [0x11438, 0x1143f], [0x11442, 0x11444], [0x11446, 0x11446], + [0x1145e, 0x1145e], [0x114b3, 0x114b8], [0x114ba, 0x114ba], + [0x114bf, 0x114c0], [0x114c2, 0x114c3], [0x115b2, 0x115b5], + [0x115bc, 0x115bd], [0x115bf, 0x115c0], [0x115dc, 0x115dd], + [0x11633, 0x1163a], [0x1163d, 0x1163d], [0x1163f, 0x11640], + [0x116ab, 0x116ab], [0x116ad, 0x116ad], [0x116b0, 0x116b5], + [0x116b7, 0x116b7], [0x1171d, 0x1171f], [0x11722, 0x11725], + [0x11727, 0x1172b], [0x1182f, 0x11837], [0x11839, 0x1183a], + [0x1193b, 0x1193c], [0x1193e, 0x1193e], [0x11943, 0x11943], + [0x119d4, 0x119d7], [0x119da, 0x119db], [0x119e0, 0x119e0], + [0x11a01, 0x11a0a], [0x11a33, 0x11a38], [0x11a3b, 0x11a3e], + [0x11a47, 0x11a47], [0x11a51, 0x11a56], [0x11a59, 0x11a5b], + [0x11a8a, 0x11a96], [0x11a98, 0x11a99], [0x11c30, 0x11c36], + [0x11c38, 0x11c3d], [0x11c3f, 0x11c3f], [0x11c92, 0x11ca7], + [0x11caa, 0x11cb0], [0x11cb2, 0x11cb3], [0x11cb5, 0x11cb6], + [0x11d31, 0x11d36], [0x11d3a, 0x11d3a], [0x11d3c, 0x11d3d], + [0x11d3f, 0x11d45], [0x11d47, 0x11d47], [0x11d90, 0x11d91], + [0x11d95, 0x11d95], [0x11d97, 0x11d97], [0x11ef3, 0x11ef4], + [0x13430, 0x13438], [0x16af0, 0x16af4], [0x16b30, 0x16b36], + [0x16f4f, 0x16f4f], [0x16f8f, 0x16f92], [0x16fe4, 0x16fe4], + [0x1bc9d, 0x1bc9e], [0x1bca0, 0x1bca3], [0x1d167, 0x1d169], + [0x1d173, 0x1d182], [0x1d185, 0x1d18b], [0x1d1aa, 0x1d1ad], + [0x1d242, 0x1d244], [0x1da00, 0x1da36], [0x1da3b, 0x1da6c], + [0x1da75, 0x1da75], [0x1da84, 0x1da84], [0x1da9b, 0x1da9f], + [0x1daa1, 0x1daaf], [0x1e000, 0x1e006], [0x1e008, 0x1e018], + [0x1e01b, 0x1e021], [0x1e023, 0x1e024], [0x1e026, 0x1e02a], + [0x1e130, 0x1e136], [0x1e2ec, 0x1e2ef], [0x1e8d0, 0x1e8d6], + [0x1e944, 0x1e94a], [0xe0001, 0xe0001], [0xe0020, 0xe007f], + [0xe0100, 0xe01ef], ]; // Sorted list of non-overlapping intervals of East Asian Ambiguous characters // generated by the `./ranges.py` helper. lib.wc.ambiguous = [ - [0x00a1, 0x00a1], [0x00a4, 0x00a4], [0x00a7, 0x00a8], - [0x00aa, 0x00aa], [0x00ad, 0x00ae], [0x00b0, 0x00b4], - [0x00b6, 0x00ba], [0x00bc, 0x00bf], [0x00c6, 0x00c6], - [0x00d0, 0x00d0], [0x00d7, 0x00d8], [0x00de, 0x00e1], - [0x00e6, 0x00e6], [0x00e8, 0x00ea], [0x00ec, 0x00ed], - [0x00f0, 0x00f0], [0x00f2, 0x00f3], [0x00f7, 0x00fa], - [0x00fc, 0x00fc], [0x00fe, 0x00fe], [0x0101, 0x0101], - [0x0111, 0x0111], [0x0113, 0x0113], [0x011b, 0x011b], - [0x0126, 0x0127], [0x012b, 0x012b], [0x0131, 0x0133], - [0x0138, 0x0138], [0x013f, 0x0142], [0x0144, 0x0144], - [0x0148, 0x014b], [0x014d, 0x014d], [0x0152, 0x0153], - [0x0166, 0x0167], [0x016b, 0x016b], [0x01ce, 0x01ce], - [0x01d0, 0x01d0], [0x01d2, 0x01d2], [0x01d4, 0x01d4], - [0x01d6, 0x01d6], [0x01d8, 0x01d8], [0x01da, 0x01da], - [0x01dc, 0x01dc], [0x0251, 0x0251], [0x0261, 0x0261], - [0x02c4, 0x02c4], [0x02c7, 0x02c7], [0x02c9, 0x02cb], - [0x02cd, 0x02cd], [0x02d0, 0x02d0], [0x02d8, 0x02db], - [0x02dd, 0x02dd], [0x02df, 0x02df], [0x0300, 0x036f], - [0x0391, 0x03a1], [0x03a3, 0x03a9], [0x03b1, 0x03c1], - [0x03c3, 0x03c9], [0x0401, 0x0401], [0x0410, 0x044f], - [0x0451, 0x0451], [0x1100, 0x115f], [0x2010, 0x2010], - [0x2013, 0x2016], [0x2018, 0x2019], [0x201c, 0x201d], - [0x2020, 0x2022], [0x2024, 0x2027], [0x2030, 0x2030], - [0x2032, 0x2033], [0x2035, 0x2035], [0x203b, 0x203b], - [0x203e, 0x203e], [0x2074, 0x2074], [0x207f, 0x207f], - [0x2081, 0x2084], [0x20ac, 0x20ac], [0x2103, 0x2103], - [0x2105, 0x2105], [0x2109, 0x2109], [0x2113, 0x2113], - [0x2116, 0x2116], [0x2121, 0x2122], [0x2126, 0x2126], - [0x212b, 0x212b], [0x2153, 0x2154], [0x215b, 0x215e], - [0x2160, 0x216b], [0x2170, 0x2179], [0x2189, 0x2189], - [0x2190, 0x2199], [0x21b8, 0x21b9], [0x21d2, 0x21d2], - [0x21d4, 0x21d4], [0x21e7, 0x21e7], [0x2200, 0x2200], - [0x2202, 0x2203], [0x2207, 0x2208], [0x220b, 0x220b], - [0x220f, 0x220f], [0x2211, 0x2211], [0x2215, 0x2215], - [0x221a, 0x221a], [0x221d, 0x2220], [0x2223, 0x2223], - [0x2225, 0x2225], [0x2227, 0x222c], [0x222e, 0x222e], - [0x2234, 0x2237], [0x223c, 0x223d], [0x2248, 0x2248], - [0x224c, 0x224c], [0x2252, 0x2252], [0x2260, 0x2261], - [0x2264, 0x2267], [0x226a, 0x226b], [0x226e, 0x226f], - [0x2282, 0x2283], [0x2286, 0x2287], [0x2295, 0x2295], - [0x2299, 0x2299], [0x22a5, 0x22a5], [0x22bf, 0x22bf], - [0x2312, 0x2312], [0x231a, 0x231b], [0x2329, 0x232a], - [0x23e9, 0x23ec], [0x23f0, 0x23f0], [0x23f3, 0x23f3], - [0x2460, 0x24e9], [0x24eb, 0x254b], [0x2550, 0x2573], - [0x2580, 0x258f], [0x2592, 0x2595], [0x25a0, 0x25a1], - [0x25a3, 0x25a9], [0x25b2, 0x25b3], [0x25b6, 0x25b7], - [0x25bc, 0x25bd], [0x25c0, 0x25c1], [0x25c6, 0x25c8], - [0x25cb, 0x25cb], [0x25ce, 0x25d1], [0x25e2, 0x25e5], - [0x25ef, 0x25ef], [0x25fd, 0x25fe], [0x2605, 0x2606], - [0x2609, 0x2609], [0x260e, 0x260f], [0x2614, 0x2615], - [0x261c, 0x261c], [0x261e, 0x261e], [0x2640, 0x2640], - [0x2642, 0x2642], [0x2648, 0x2653], [0x2660, 0x2661], - [0x2663, 0x2665], [0x2667, 0x266a], [0x266c, 0x266d], - [0x266f, 0x266f], [0x267f, 0x267f], [0x2693, 0x2693], - [0x269e, 0x269f], [0x26a1, 0x26a1], [0x26aa, 0x26ab], - [0x26bd, 0x26bf], [0x26c4, 0x26e1], [0x26e3, 0x26e3], - [0x26e8, 0x26ff], [0x2705, 0x2705], [0x270a, 0x270b], - [0x2728, 0x2728], [0x273d, 0x273d], [0x274c, 0x274c], - [0x274e, 0x274e], [0x2753, 0x2755], [0x2757, 0x2757], - [0x2776, 0x277f], [0x2795, 0x2797], [0x27b0, 0x27b0], - [0x27bf, 0x27bf], [0x2b1b, 0x2b1c], [0x2b50, 0x2b50], - [0x2b55, 0x2b59], [0x2e80, 0x2fdf], [0x2ff0, 0x303e], - [0x3040, 0x4dbf], [0x4e00, 0xa4cf], [0xa960, 0xa97f], - [0xac00, 0xd7a3], [0xe000, 0xfaff], [0xfe00, 0xfe19], - [0xfe30, 0xfe6f], [0xff01, 0xff60], [0xffe0, 0xffe6], - [0xfffd, 0xfffd], [0x16fe0, 0x16fe3], [0x17000, 0x18aff], - [0x1b000, 0x1b12f], [0x1b150, 0x1b152], [0x1b164, 0x1b167], - [0x1b170, 0x1b2ff], [0x1f004, 0x1f004], [0x1f0cf, 0x1f0cf], - [0x1f100, 0x1f10a], [0x1f110, 0x1f12d], [0x1f130, 0x1f169], - [0x1f170, 0x1f1ac], [0x1f200, 0x1f202], [0x1f210, 0x1f23b], - [0x1f240, 0x1f248], [0x1f250, 0x1f251], [0x1f260, 0x1f265], - [0x1f300, 0x1f320], [0x1f32d, 0x1f335], [0x1f337, 0x1f37c], - [0x1f37e, 0x1f393], [0x1f3a0, 0x1f3ca], [0x1f3cf, 0x1f3d3], - [0x1f3e0, 0x1f3f0], [0x1f3f4, 0x1f3f4], [0x1f3f8, 0x1f43e], - [0x1f440, 0x1f440], [0x1f442, 0x1f4fc], [0x1f4ff, 0x1f53d], - [0x1f54b, 0x1f54e], [0x1f550, 0x1f567], [0x1f57a, 0x1f57a], - [0x1f595, 0x1f596], [0x1f5a4, 0x1f5a4], [0x1f5fb, 0x1f64f], - [0x1f680, 0x1f6c5], [0x1f6cc, 0x1f6cc], [0x1f6d0, 0x1f6d2], - [0x1f6d5, 0x1f6d5], [0x1f6eb, 0x1f6ec], [0x1f6f4, 0x1f6fa], - [0x1f7e0, 0x1f7eb], [0x1f90d, 0x1f971], [0x1f973, 0x1f976], - [0x1f97a, 0x1f9a2], [0x1f9a5, 0x1f9aa], [0x1f9ae, 0x1f9ca], - [0x1f9cd, 0x1f9ff], [0x1fa70, 0x1fa73], [0x1fa78, 0x1fa7a], - [0x1fa80, 0x1fa82], [0x1fa90, 0x1fa95], [0x20000, 0x2fffd], - [0x30000, 0x3fffd], [0xe0100, 0xe01ef], [0xf0000, 0xffffd], - [0x100000, 0x10fffd], + [0x00a1, 0x00a1], [0x00a4, 0x00a4], [0x00a7, 0x00a8], + [0x00aa, 0x00aa], [0x00ad, 0x00ae], [0x00b0, 0x00b4], + [0x00b6, 0x00ba], [0x00bc, 0x00bf], [0x00c6, 0x00c6], + [0x00d0, 0x00d0], [0x00d7, 0x00d8], [0x00de, 0x00e1], + [0x00e6, 0x00e6], [0x00e8, 0x00ea], [0x00ec, 0x00ed], + [0x00f0, 0x00f0], [0x00f2, 0x00f3], [0x00f7, 0x00fa], + [0x00fc, 0x00fc], [0x00fe, 0x00fe], [0x0101, 0x0101], + [0x0111, 0x0111], [0x0113, 0x0113], [0x011b, 0x011b], + [0x0126, 0x0127], [0x012b, 0x012b], [0x0131, 0x0133], + [0x0138, 0x0138], [0x013f, 0x0142], [0x0144, 0x0144], + [0x0148, 0x014b], [0x014d, 0x014d], [0x0152, 0x0153], + [0x0166, 0x0167], [0x016b, 0x016b], [0x01ce, 0x01ce], + [0x01d0, 0x01d0], [0x01d2, 0x01d2], [0x01d4, 0x01d4], + [0x01d6, 0x01d6], [0x01d8, 0x01d8], [0x01da, 0x01da], + [0x01dc, 0x01dc], [0x0251, 0x0251], [0x0261, 0x0261], + [0x02c4, 0x02c4], [0x02c7, 0x02c7], [0x02c9, 0x02cb], + [0x02cd, 0x02cd], [0x02d0, 0x02d0], [0x02d8, 0x02db], + [0x02dd, 0x02dd], [0x02df, 0x02df], [0x0300, 0x036f], + [0x0391, 0x03a1], [0x03a3, 0x03a9], [0x03b1, 0x03c1], + [0x03c3, 0x03c9], [0x0401, 0x0401], [0x0410, 0x044f], + [0x0451, 0x0451], [0x1100, 0x115f], [0x2010, 0x2010], + [0x2013, 0x2016], [0x2018, 0x2019], [0x201c, 0x201d], + [0x2020, 0x2022], [0x2024, 0x2027], [0x2030, 0x2030], + [0x2032, 0x2033], [0x2035, 0x2035], [0x203b, 0x203b], + [0x203e, 0x203e], [0x2074, 0x2074], [0x207f, 0x207f], + [0x2081, 0x2084], [0x20ac, 0x20ac], [0x2103, 0x2103], + [0x2105, 0x2105], [0x2109, 0x2109], [0x2113, 0x2113], + [0x2116, 0x2116], [0x2121, 0x2122], [0x2126, 0x2126], + [0x212b, 0x212b], [0x2153, 0x2154], [0x215b, 0x215e], + [0x2160, 0x216b], [0x2170, 0x2179], [0x2189, 0x2189], + [0x2190, 0x2199], [0x21b8, 0x21b9], [0x21d2, 0x21d2], + [0x21d4, 0x21d4], [0x21e7, 0x21e7], [0x2200, 0x2200], + [0x2202, 0x2203], [0x2207, 0x2208], [0x220b, 0x220b], + [0x220f, 0x220f], [0x2211, 0x2211], [0x2215, 0x2215], + [0x221a, 0x221a], [0x221d, 0x2220], [0x2223, 0x2223], + [0x2225, 0x2225], [0x2227, 0x222c], [0x222e, 0x222e], + [0x2234, 0x2237], [0x223c, 0x223d], [0x2248, 0x2248], + [0x224c, 0x224c], [0x2252, 0x2252], [0x2260, 0x2261], + [0x2264, 0x2267], [0x226a, 0x226b], [0x226e, 0x226f], + [0x2282, 0x2283], [0x2286, 0x2287], [0x2295, 0x2295], + [0x2299, 0x2299], [0x22a5, 0x22a5], [0x22bf, 0x22bf], + [0x2312, 0x2312], [0x231a, 0x231b], [0x2329, 0x232a], + [0x23e9, 0x23ec], [0x23f0, 0x23f0], [0x23f3, 0x23f3], + [0x2460, 0x24e9], [0x24eb, 0x254b], [0x2550, 0x2573], + [0x2580, 0x258f], [0x2592, 0x2595], [0x25a0, 0x25a1], + [0x25a3, 0x25a9], [0x25b2, 0x25b3], [0x25b6, 0x25b7], + [0x25bc, 0x25bd], [0x25c0, 0x25c1], [0x25c6, 0x25c8], + [0x25cb, 0x25cb], [0x25ce, 0x25d1], [0x25e2, 0x25e5], + [0x25ef, 0x25ef], [0x25fd, 0x25fe], [0x2605, 0x2606], + [0x2609, 0x2609], [0x260e, 0x260f], [0x2614, 0x2615], + [0x261c, 0x261c], [0x261e, 0x261e], [0x2640, 0x2640], + [0x2642, 0x2642], [0x2648, 0x2653], [0x2660, 0x2661], + [0x2663, 0x2665], [0x2667, 0x266a], [0x266c, 0x266d], + [0x266f, 0x266f], [0x267f, 0x267f], [0x2693, 0x2693], + [0x269e, 0x269f], [0x26a1, 0x26a1], [0x26aa, 0x26ab], + [0x26bd, 0x26bf], [0x26c4, 0x26e1], [0x26e3, 0x26e3], + [0x26e8, 0x26ff], [0x2705, 0x2705], [0x270a, 0x270b], + [0x2728, 0x2728], [0x273d, 0x273d], [0x274c, 0x274c], + [0x274e, 0x274e], [0x2753, 0x2755], [0x2757, 0x2757], + [0x2776, 0x277f], [0x2795, 0x2797], [0x27b0, 0x27b0], + [0x27bf, 0x27bf], [0x2b1b, 0x2b1c], [0x2b50, 0x2b50], + [0x2b55, 0x2b59], [0x2e80, 0x2fdf], [0x2ff0, 0x303e], + [0x3040, 0x4dbf], [0x4e00, 0xa4cf], [0xa960, 0xa97f], + [0xac00, 0xd7a3], [0xe000, 0xfaff], [0xfe00, 0xfe19], + [0xfe30, 0xfe6f], [0xff01, 0xff60], [0xffe0, 0xffe6], + [0xfffd, 0xfffd], [0x16fe0, 0x16fe4], [0x16ff0, 0x16ff1], + [0x17000, 0x18cd5], [0x18d00, 0x18d08], [0x1b000, 0x1b12f], + [0x1b150, 0x1b152], [0x1b164, 0x1b167], [0x1b170, 0x1b2ff], + [0x1f004, 0x1f004], [0x1f0cf, 0x1f0cf], [0x1f100, 0x1f10a], + [0x1f110, 0x1f12d], [0x1f130, 0x1f169], [0x1f170, 0x1f1ac], + [0x1f200, 0x1f202], [0x1f210, 0x1f23b], [0x1f240, 0x1f248], + [0x1f250, 0x1f251], [0x1f260, 0x1f265], [0x1f300, 0x1f320], + [0x1f32d, 0x1f335], [0x1f337, 0x1f37c], [0x1f37e, 0x1f393], + [0x1f3a0, 0x1f3ca], [0x1f3cf, 0x1f3d3], [0x1f3e0, 0x1f3f0], + [0x1f3f4, 0x1f3f4], [0x1f3f8, 0x1f43e], [0x1f440, 0x1f440], + [0x1f442, 0x1f4fc], [0x1f4ff, 0x1f53d], [0x1f54b, 0x1f54e], + [0x1f550, 0x1f567], [0x1f57a, 0x1f57a], [0x1f595, 0x1f596], + [0x1f5a4, 0x1f5a4], [0x1f5fb, 0x1f64f], [0x1f680, 0x1f6c5], + [0x1f6cc, 0x1f6cc], [0x1f6d0, 0x1f6d2], [0x1f6d5, 0x1f6d7], + [0x1f6eb, 0x1f6ec], [0x1f6f4, 0x1f6fc], [0x1f7e0, 0x1f7eb], + [0x1f90c, 0x1f93a], [0x1f93c, 0x1f945], [0x1f947, 0x1f978], + [0x1f97a, 0x1f9cb], [0x1f9cd, 0x1f9ff], [0x1fa70, 0x1fa74], + [0x1fa78, 0x1fa7a], [0x1fa80, 0x1fa86], [0x1fa90, 0x1faa8], + [0x1fab0, 0x1fab6], [0x1fac0, 0x1fac2], [0x1fad0, 0x1fad6], + [0x20000, 0x2fffd], [0x30000, 0x3fffd], [0xe0100, 0xe01ef], + [0xf0000, 0xffffd], [0x100000, 0x10fffd], ]; // Sorted list of non-overlapping intervals of East Asian Unambiguous characters // generated by the `./ranges.py` helper. lib.wc.unambiguous = [ - [0x1100, 0x115f], [0x231a, 0x231b], [0x2329, 0x232a], - [0x23e9, 0x23ec], [0x23f0, 0x23f0], [0x23f3, 0x23f3], - [0x25fd, 0x25fe], [0x2614, 0x2615], [0x2648, 0x2653], - [0x267f, 0x267f], [0x2693, 0x2693], [0x26a1, 0x26a1], - [0x26aa, 0x26ab], [0x26bd, 0x26be], [0x26c4, 0x26c5], - [0x26ce, 0x26ce], [0x26d4, 0x26d4], [0x26ea, 0x26ea], - [0x26f2, 0x26f3], [0x26f5, 0x26f5], [0x26fa, 0x26fa], - [0x26fd, 0x26fd], [0x2705, 0x2705], [0x270a, 0x270b], - [0x2728, 0x2728], [0x274c, 0x274c], [0x274e, 0x274e], - [0x2753, 0x2755], [0x2757, 0x2757], [0x2795, 0x2797], - [0x27b0, 0x27b0], [0x27bf, 0x27bf], [0x2b1b, 0x2b1c], - [0x2b50, 0x2b50], [0x2b55, 0x2b55], [0x2e80, 0x2fdf], - [0x2ff0, 0x303e], [0x3040, 0x3247], [0x3250, 0x4dbf], - [0x4e00, 0xa4cf], [0xa960, 0xa97f], [0xac00, 0xd7a3], - [0xf900, 0xfaff], [0xfe10, 0xfe19], [0xfe30, 0xfe6f], - [0xff01, 0xff60], [0xffe0, 0xffe6], [0x16fe0, 0x16fe3], - [0x17000, 0x18aff], [0x1b000, 0x1b12f], [0x1b150, 0x1b152], - [0x1b164, 0x1b167], [0x1b170, 0x1b2ff], [0x1f004, 0x1f004], - [0x1f0cf, 0x1f0cf], [0x1f18e, 0x1f18e], [0x1f191, 0x1f19a], - [0x1f200, 0x1f202], [0x1f210, 0x1f23b], [0x1f240, 0x1f248], - [0x1f250, 0x1f251], [0x1f260, 0x1f265], [0x1f300, 0x1f320], - [0x1f32d, 0x1f335], [0x1f337, 0x1f37c], [0x1f37e, 0x1f393], - [0x1f3a0, 0x1f3ca], [0x1f3cf, 0x1f3d3], [0x1f3e0, 0x1f3f0], - [0x1f3f4, 0x1f3f4], [0x1f3f8, 0x1f43e], [0x1f440, 0x1f440], - [0x1f442, 0x1f4fc], [0x1f4ff, 0x1f53d], [0x1f54b, 0x1f54e], - [0x1f550, 0x1f567], [0x1f57a, 0x1f57a], [0x1f595, 0x1f596], - [0x1f5a4, 0x1f5a4], [0x1f5fb, 0x1f64f], [0x1f680, 0x1f6c5], - [0x1f6cc, 0x1f6cc], [0x1f6d0, 0x1f6d2], [0x1f6d5, 0x1f6d5], - [0x1f6eb, 0x1f6ec], [0x1f6f4, 0x1f6fa], [0x1f7e0, 0x1f7eb], - [0x1f90d, 0x1f971], [0x1f973, 0x1f976], [0x1f97a, 0x1f9a2], - [0x1f9a5, 0x1f9aa], [0x1f9ae, 0x1f9ca], [0x1f9cd, 0x1f9ff], - [0x1fa70, 0x1fa73], [0x1fa78, 0x1fa7a], [0x1fa80, 0x1fa82], - [0x1fa90, 0x1fa95], [0x20000, 0x2fffd], [0x30000, 0x3fffd], + [0x1100, 0x115f], [0x231a, 0x231b], [0x2329, 0x232a], + [0x23e9, 0x23ec], [0x23f0, 0x23f0], [0x23f3, 0x23f3], + [0x25fd, 0x25fe], [0x2614, 0x2615], [0x2648, 0x2653], + [0x267f, 0x267f], [0x2693, 0x2693], [0x26a1, 0x26a1], + [0x26aa, 0x26ab], [0x26bd, 0x26be], [0x26c4, 0x26c5], + [0x26ce, 0x26ce], [0x26d4, 0x26d4], [0x26ea, 0x26ea], + [0x26f2, 0x26f3], [0x26f5, 0x26f5], [0x26fa, 0x26fa], + [0x26fd, 0x26fd], [0x2705, 0x2705], [0x270a, 0x270b], + [0x2728, 0x2728], [0x274c, 0x274c], [0x274e, 0x274e], + [0x2753, 0x2755], [0x2757, 0x2757], [0x2795, 0x2797], + [0x27b0, 0x27b0], [0x27bf, 0x27bf], [0x2b1b, 0x2b1c], + [0x2b50, 0x2b50], [0x2b55, 0x2b55], [0x2e80, 0x2fdf], + [0x2ff0, 0x303e], [0x3040, 0x3247], [0x3250, 0x4dbf], + [0x4e00, 0xa4cf], [0xa960, 0xa97f], [0xac00, 0xd7a3], + [0xf900, 0xfaff], [0xfe10, 0xfe19], [0xfe30, 0xfe6f], + [0xff01, 0xff60], [0xffe0, 0xffe6], [0x16fe0, 0x16fe4], + [0x16ff0, 0x16ff1], [0x17000, 0x18cd5], [0x18d00, 0x18d08], + [0x1b000, 0x1b12f], [0x1b150, 0x1b152], [0x1b164, 0x1b167], + [0x1b170, 0x1b2ff], [0x1f004, 0x1f004], [0x1f0cf, 0x1f0cf], + [0x1f18e, 0x1f18e], [0x1f191, 0x1f19a], [0x1f200, 0x1f202], + [0x1f210, 0x1f23b], [0x1f240, 0x1f248], [0x1f250, 0x1f251], + [0x1f260, 0x1f265], [0x1f300, 0x1f320], [0x1f32d, 0x1f335], + [0x1f337, 0x1f37c], [0x1f37e, 0x1f393], [0x1f3a0, 0x1f3ca], + [0x1f3cf, 0x1f3d3], [0x1f3e0, 0x1f3f0], [0x1f3f4, 0x1f3f4], + [0x1f3f8, 0x1f43e], [0x1f440, 0x1f440], [0x1f442, 0x1f4fc], + [0x1f4ff, 0x1f53d], [0x1f54b, 0x1f54e], [0x1f550, 0x1f567], + [0x1f57a, 0x1f57a], [0x1f595, 0x1f596], [0x1f5a4, 0x1f5a4], + [0x1f5fb, 0x1f64f], [0x1f680, 0x1f6c5], [0x1f6cc, 0x1f6cc], + [0x1f6d0, 0x1f6d2], [0x1f6d5, 0x1f6d7], [0x1f6eb, 0x1f6ec], + [0x1f6f4, 0x1f6fc], [0x1f7e0, 0x1f7eb], [0x1f90c, 0x1f93a], + [0x1f93c, 0x1f945], [0x1f947, 0x1f978], [0x1f97a, 0x1f9cb], + [0x1f9cd, 0x1f9ff], [0x1fa70, 0x1fa74], [0x1fa78, 0x1fa7a], + [0x1fa80, 0x1fa86], [0x1fa90, 0x1faa8], [0x1fab0, 0x1fab6], + [0x1fac0, 0x1fac2], [0x1fad0, 0x1fad6], [0x20000, 0x2fffd], + [0x30000, 0x3fffd], ]; /** * Binary search to check if the given unicode character is in the table. * - * @param {integer} ucs A unicode character code. - * @param {Object} table A sorted list of internals to match against. + * @param {number} ucs A unicode character code. + * @param {!Array>} table A sorted list of internals to match + * against. * @return {boolean} True if the given character is in the table. */ lib.wc.binaryTableSearch_ = function(ucs, table) { - var min = 0, max = table.length - 1; - var mid; + let min = 0; + let max = table.length - 1; + let mid; - if (ucs < table[min][0] || ucs > table[max][1]) + if (ucs < table[min][0] || ucs > table[max][1]) { return false; + } while (max >= min) { mid = Math.floor((min + max) / 2); if (ucs > table[mid][1]) { @@ -4292,8 +4996,7 @@ lib.wc.binaryTableSearch_ = function(ucs, table) { /** * Binary search to check if the given unicode character is a space character. * - * @param {integer} ucs A unicode character code. - * + * @param {number} ucs A unicode character code. * @return {boolean} True if the given character is a space character; false * otherwise. */ @@ -4305,10 +5008,9 @@ lib.wc.isSpace = function(ucs) { * Auxiliary function for checking if the given unicode character is a East * Asian Ambiguous character. * - * @param {integer} ucs A unicode character code. - * + * @param {number} ucs A unicode character code. * @return {boolean} True if the given character is a East Asian Ambiguous - * character. + * character. */ lib.wc.isCjkAmbiguous = function(ucs) { return lib.wc.binaryTableSearch_(ucs, lib.wc.ambiguous); @@ -4317,9 +5019,8 @@ lib.wc.isCjkAmbiguous = function(ucs) { /** * Determine the column width of the given character. * - * @param {integer} ucs A unicode character code. - * - * @return {integer} The column width of the given character. + * @param {number} ucs A unicode character code. + * @return {number} The column width of the given character. */ lib.wc.charWidth = function(ucs) { if (lib.wc.regardCjkAmbiguous) { @@ -4333,28 +5034,30 @@ lib.wc.charWidth = function(ucs) { * Determine the column width of the given character without considering East * Asian Ambiguous characters. * - * @param {integer} ucs A unicode character code. - * - * @return {integer} The column width of the given character. + * @param {number} ucs A unicode character code. + * @return {number} The column width of the given character. */ lib.wc.charWidthDisregardAmbiguous = function(ucs) { // Optimize for ASCII characters. if (ucs < 0x7f) { - if (ucs >= 0x20) + if (ucs >= 0x20) { return 1; - else if (ucs == 0) + } else if (ucs == 0) { return lib.wc.nulWidth; - else /* if (ucs < 0x20) */ + } else /* if (ucs < 0x20) */ { return lib.wc.controlWidth; + } } // Test for 8-bit control characters. - if (ucs < 0xa0) + if (ucs < 0xa0) { return lib.wc.controlWidth; + } // Binary search in table of non-spacing characters. - if (lib.wc.isSpace(ucs)) + if (lib.wc.isSpace(ucs)) { return 0; + } // Binary search in table of wide characters. return lib.wc.binaryTableSearch_(ucs, lib.wc.unambiguous) ? 2 : 1; @@ -4364,13 +5067,13 @@ lib.wc.charWidthDisregardAmbiguous = function(ucs) { * Determine the column width of the given character considering East Asian * Ambiguous characters. * - * @param {integer} ucs A unicode character code. - * - * @return {integer} The column width of the given character. + * @param {number} ucs A unicode character code. + * @return {number} The column width of the given character. */ lib.wc.charWidthRegardAmbiguous = function(ucs) { - if (lib.wc.isCjkAmbiguous(ucs)) + if (lib.wc.isCjkAmbiguous(ucs)) { return lib.wc.cjkAmbiguousWidth; + } return lib.wc.charWidthDisregardAmbiguous(ucs); }; @@ -4379,17 +5082,17 @@ lib.wc.charWidthRegardAmbiguous = function(ucs) { * Determine the column width of the given string. * * @param {string} str A string. - * - * @return {integer} The column width of the given string. + * @return {number} The column width of the given string. */ lib.wc.strWidth = function(str) { - var width, rv = 0; + let rv = 0; - for (var i = 0; i < str.length;) { - var codePoint = str.codePointAt(i); - width = lib.wc.charWidth(codePoint); - if (width < 0) + for (let i = 0; i < str.length;) { + const codePoint = str.codePointAt(i); + const width = lib.wc.charWidth(codePoint); + if (width < 0) { return -1; + } rv += width; i += (codePoint <= 0xffff) ? 1 : 2; } @@ -4401,14 +5104,12 @@ lib.wc.strWidth = function(str) { * Get the substring at the given column offset of the given column width. * * @param {string} str The string to get substring from. - * @param {integer} start The starting column offset to get substring. - * @param {integer} opt_width The column width of the substring. - * + * @param {number} start The starting column offset to get substring. + * @param {number=} subwidth The column width of the substring. * @return {string} The substring. */ -lib.wc.substr = function(str, start, opt_width) { - var startIndex = 0; - var endIndex, width; +lib.wc.substr = function(str, start, subwidth = undefined) { + let startIndex = 0; // Fun edge case: Normally we associate zero width codepoints (like combining // characters) with the previous codepoint, so we skip any leading ones while @@ -4417,21 +5118,24 @@ lib.wc.substr = function(str, start, opt_width) { // in the result. This also makes for a simple optimization for a common // request. if (start) { - for (width = 0; startIndex < str.length;) { + for (let width = 0; startIndex < str.length;) { const codePoint = str.codePointAt(startIndex); width += lib.wc.charWidth(codePoint); - if (width > start) + if (width > start) { break; + } startIndex += (codePoint <= 0xffff) ? 1 : 2; } } - if (opt_width != undefined) { - for (endIndex = startIndex, width = 0; endIndex < str.length;) { + if (subwidth !== undefined) { + let endIndex = startIndex; + for (let width = 0; endIndex < str.length;) { const codePoint = str.codePointAt(endIndex); width += lib.wc.charWidth(codePoint); - if (width > opt_width) + if (width > subwidth) { break; + } endIndex += (codePoint <= 0xffff) ? 1 : 2; } return str.substring(startIndex, endIndex); @@ -4444,304 +5148,31 @@ lib.wc.substr = function(str, start, opt_width) { * Get substring at the given start and end column offset. * * @param {string} str The string to get substring from. - * @param {integer} start The starting column offset. - * @param {integer} end The ending column offset. - * + * @param {number} start The starting column offset. + * @param {number} end The ending column offset. * @return {string} The substring. */ lib.wc.substring = function(str, start, end) { return lib.wc.substr(str, start, end - start); }; lib.resource.add('libdot/changelog/version', 'text/plain', -'2.0.0' +'8.0.0' ); lib.resource.add('libdot/changelog/date', 'text/plain', -'2019-06-17' +'2020-12-07' ); -// This file was generated by libdot/bin/concat.sh. -// It has been marked read-only for your safety. Rather than -// edit it directly, please modify one of these source files. -// -// hterm/audio/bell.ogg -// hterm/images/icon-96.png +// SOURCE FILE: hterm/js/hterm.js +// Copyright (c) 2012 The Chromium OS Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. -'use strict'; - -lib.resource.add('hterm/audio/bell', 'audio/ogg;base64', -'T2dnUwACAAAAAAAAAADhqW5KAAAAAMFvEjYBHgF2b3JiaXMAAAAAAYC7AAAAAAAAAHcBAAAAAAC4' + -'AU9nZ1MAAAAAAAAAAAAA4aluSgEAAAAAesI3EC3//////////////////8kDdm9yYmlzHQAAAFhp' + -'cGguT3JnIGxpYlZvcmJpcyBJIDIwMDkwNzA5AAAAAAEFdm9yYmlzKUJDVgEACAAAADFMIMWA0JBV' + -'AAAQAABgJCkOk2ZJKaWUoSh5mJRISSmllMUwiZiUicUYY4wxxhhjjDHGGGOMIDRkFQAABACAKAmO' + -'o+ZJas45ZxgnjnKgOWlOOKcgB4pR4DkJwvUmY26mtKZrbs4pJQgNWQUAAAIAQEghhRRSSCGFFGKI' + -'IYYYYoghhxxyyCGnnHIKKqigggoyyCCDTDLppJNOOumoo4466ii00EILLbTSSkwx1VZjrr0GXXxz' + -'zjnnnHPOOeecc84JQkNWAQAgAAAEQgYZZBBCCCGFFFKIKaaYcgoyyIDQkFUAACAAgAAAAABHkRRJ' + -'sRTLsRzN0SRP8ixREzXRM0VTVE1VVVVVdV1XdmXXdnXXdn1ZmIVbuH1ZuIVb2IVd94VhGIZhGIZh' + -'GIZh+H3f933f930gNGQVACABAKAjOZbjKaIiGqLiOaIDhIasAgBkAAAEACAJkiIpkqNJpmZqrmmb' + -'tmirtm3LsizLsgyEhqwCAAABAAQAAAAAAKBpmqZpmqZpmqZpmqZpmqZpmqZpmmZZlmVZlmVZlmVZ' + -'lmVZlmVZlmVZlmVZlmVZlmVZlmVZlmVZlmVZQGjIKgBAAgBAx3Ecx3EkRVIkx3IsBwgNWQUAyAAA' + -'CABAUizFcjRHczTHczzHczxHdETJlEzN9EwPCA1ZBQAAAgAIAAAAAABAMRzFcRzJ0SRPUi3TcjVX' + -'cz3Xc03XdV1XVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVYHQkFUAAAQAACGdZpZq' + -'gAgzkGEgNGQVAIAAAAAYoQhDDAgNWQUAAAQAAIih5CCa0JrzzTkOmuWgqRSb08GJVJsnuamYm3PO' + -'OeecbM4Z45xzzinKmcWgmdCac85JDJqloJnQmnPOeRKbB62p0ppzzhnnnA7GGWGcc85p0poHqdlY' + -'m3POWdCa5qi5FJtzzomUmye1uVSbc84555xzzjnnnHPOqV6czsE54Zxzzonam2u5CV2cc875ZJzu' + -'zQnhnHPOOeecc84555xzzglCQ1YBAEAAAARh2BjGnYIgfY4GYhQhpiGTHnSPDpOgMcgppB6NjkZK' + -'qYNQUhknpXSC0JBVAAAgAACEEFJIIYUUUkghhRRSSCGGGGKIIaeccgoqqKSSiirKKLPMMssss8wy' + -'y6zDzjrrsMMQQwwxtNJKLDXVVmONteaec645SGultdZaK6WUUkoppSA0ZBUAAAIAQCBkkEEGGYUU' + -'UkghhphyyimnoIIKCA1ZBQAAAgAIAAAA8CTPER3RER3RER3RER3RER3P8RxREiVREiXRMi1TMz1V' + -'VFVXdm1Zl3Xbt4Vd2HXf133f141fF4ZlWZZlWZZlWZZlWZZlWZZlCUJDVgEAIAAAAEIIIYQUUkgh' + -'hZRijDHHnINOQgmB0JBVAAAgAIAAAAAAR3EUx5EcyZEkS7IkTdIszfI0T/M00RNFUTRNUxVd0RV1' + -'0xZlUzZd0zVl01Vl1XZl2bZlW7d9WbZ93/d93/d93/d93/d939d1IDRkFQAgAQCgIzmSIimSIjmO' + -'40iSBISGrAIAZAAABACgKI7iOI4jSZIkWZImeZZniZqpmZ7pqaIKhIasAgAAAQAEAAAAAACgaIqn' + -'mIqniIrniI4oiZZpiZqquaJsyq7ruq7ruq7ruq7ruq7ruq7ruq7ruq7ruq7ruq7ruq7ruq7rukBo' + -'yCoAQAIAQEdyJEdyJEVSJEVyJAcIDVkFAMgAAAgAwDEcQ1Ikx7IsTfM0T/M00RM90TM9VXRFFwgN' + -'WQUAAAIACAAAAAAAwJAMS7EczdEkUVIt1VI11VItVVQ9VVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVV' + -'VVVVVVVVVVVV1TRN0zSB0JCVAAAZAAAjQQYZhBCKcpBCbj1YCDHmJAWhOQahxBiEpxAzDDkNInSQ' + -'QSc9uJI5wwzz4FIoFURMg40lN44gDcKmXEnlOAhCQ1YEAFEAAIAxyDHEGHLOScmgRM4xCZ2UyDkn' + -'pZPSSSktlhgzKSWmEmPjnKPSScmklBhLip2kEmOJrQAAgAAHAIAAC6HQkBUBQBQAAGIMUgophZRS' + -'zinmkFLKMeUcUko5p5xTzjkIHYTKMQadgxAppRxTzinHHITMQeWcg9BBKAAAIMABACDAQig0ZEUA' + -'ECcA4HAkz5M0SxQlSxNFzxRl1xNN15U0zTQ1UVRVyxNV1VRV2xZNVbYlTRNNTfRUVRNFVRVV05ZN' + -'VbVtzzRl2VRV3RZV1bZl2xZ+V5Z13zNNWRZV1dZNVbV115Z9X9ZtXZg0zTQ1UVRVTRRV1VRV2zZV' + -'17Y1UXRVUVVlWVRVWXZlWfdVV9Z9SxRV1VNN2RVVVbZV2fVtVZZ94XRVXVdl2fdVWRZ+W9eF4fZ9' + -'4RhV1dZN19V1VZZ9YdZlYbd13yhpmmlqoqiqmiiqqqmqtm2qrq1bouiqoqrKsmeqrqzKsq+rrmzr' + -'miiqrqiqsiyqqiyrsqz7qizrtqiquq3KsrCbrqvrtu8LwyzrunCqrq6rsuz7qizruq3rxnHrujB8' + -'pinLpqvquqm6um7runHMtm0co6rqvirLwrDKsu/rui+0dSFRVXXdlF3jV2VZ921fd55b94WybTu/' + -'rfvKceu60vg5z28cubZtHLNuG7+t+8bzKz9hOI6lZ5q2baqqrZuqq+uybivDrOtCUVV9XZVl3zdd' + -'WRdu3zeOW9eNoqrquirLvrDKsjHcxm8cuzAcXds2jlvXnbKtC31jyPcJz2vbxnH7OuP2daOvDAnH' + -'jwAAgAEHAIAAE8pAoSErAoA4AQAGIecUUxAqxSB0EFLqIKRUMQYhc05KxRyUUEpqIZTUKsYgVI5J' + -'yJyTEkpoKZTSUgehpVBKa6GU1lJrsabUYu0gpBZKaS2U0lpqqcbUWowRYxAy56RkzkkJpbQWSmkt' + -'c05K56CkDkJKpaQUS0otVsxJyaCj0kFIqaQSU0mptVBKa6WkFktKMbYUW24x1hxKaS2kEltJKcYU' + -'U20txpojxiBkzknJnJMSSmktlNJa5ZiUDkJKmYOSSkqtlZJSzJyT0kFIqYOOSkkptpJKTKGU1kpK' + -'sYVSWmwx1pxSbDWU0lpJKcaSSmwtxlpbTLV1EFoLpbQWSmmttVZraq3GUEprJaUYS0qxtRZrbjHm' + -'GkppraQSW0mpxRZbji3GmlNrNabWam4x5hpbbT3WmnNKrdbUUo0txppjbb3VmnvvIKQWSmktlNJi' + -'ai3G1mKtoZTWSiqxlZJabDHm2lqMOZTSYkmpxZJSjC3GmltsuaaWamwx5ppSi7Xm2nNsNfbUWqwt' + -'xppTS7XWWnOPufVWAADAgAMAQIAJZaDQkJUAQBQAAEGIUs5JaRByzDkqCULMOSepckxCKSlVzEEI' + -'JbXOOSkpxdY5CCWlFksqLcVWaykptRZrLQAAoMABACDABk2JxQEKDVkJAEQBACDGIMQYhAYZpRiD' + -'0BikFGMQIqUYc05KpRRjzknJGHMOQioZY85BKCmEUEoqKYUQSkklpQIAAAocAAACbNCUWByg0JAV' + -'AUAUAABgDGIMMYYgdFQyKhGETEonqYEQWgutddZSa6XFzFpqrbTYQAithdYySyXG1FpmrcSYWisA' + -'AOzAAQDswEIoNGQlAJAHAEAYoxRjzjlnEGLMOegcNAgx5hyEDirGnIMOQggVY85BCCGEzDkIIYQQ' + -'QuYchBBCCKGDEEIIpZTSQQghhFJK6SCEEEIppXQQQgihlFIKAAAqcAAACLBRZHOCkaBCQ1YCAHkA' + -'AIAxSjkHoZRGKcYglJJSoxRjEEpJqXIMQikpxVY5B6GUlFrsIJTSWmw1dhBKaS3GWkNKrcVYa64h' + -'pdZirDXX1FqMteaaa0otxlprzbkAANwFBwCwAxtFNicYCSo0ZCUAkAcAgCCkFGOMMYYUYoox55xD' + -'CCnFmHPOKaYYc84555RijDnnnHOMMeecc845xphzzjnnHHPOOeecc44555xzzjnnnHPOOeecc845' + -'55xzzgkAACpwAAAIsFFkc4KRoEJDVgIAqQAAABFWYowxxhgbCDHGGGOMMUYSYowxxhhjbDHGGGOM' + -'McaYYowxxhhjjDHGGGOMMcYYY4wxxhhjjDHGGGOMMcYYY4wxxhhjjDHGGGOMMcYYY4wxxhhjjDHG' + -'GFtrrbXWWmuttdZaa6211lprrQBAvwoHAP8HG1ZHOCkaCyw0ZCUAEA4AABjDmHOOOQYdhIYp6KSE' + -'DkIIoUNKOSglhFBKKSlzTkpKpaSUWkqZc1JSKiWlllLqIKTUWkottdZaByWl1lJqrbXWOgiltNRa' + -'a6212EFIKaXWWostxlBKSq212GKMNYZSUmqtxdhirDGk0lJsLcYYY6yhlNZaazHGGGstKbXWYoy1' + -'xlprSam11mKLNdZaCwDgbnAAgEiwcYaVpLPC0eBCQ1YCACEBAARCjDnnnHMQQgghUoox56CDEEII' + -'IURKMeYcdBBCCCGEjDHnoIMQQgghhJAx5hx0EEIIIYQQOucchBBCCKGEUkrnHHQQQgghlFBC6SCE' + -'EEIIoYRSSikdhBBCKKGEUkopJYQQQgmllFJKKaWEEEIIoYQSSimllBBCCKWUUkoppZQSQgghlFJK' + -'KaWUUkIIoZRQSimllFJKCCGEUkoppZRSSgkhhFBKKaWUUkopIYQSSimllFJKKaUAAIADBwCAACPo' + -'JKPKImw04cIDUGjISgCADAAAcdhq6ynWyCDFnISWS4SQchBiLhFSijlHsWVIGcUY1ZQxpRRTUmvo' + -'nGKMUU+dY0oxw6yUVkookYLScqy1dswBAAAgCAAwECEzgUABFBjIAIADhAQpAKCwwNAxXAQE5BIy' + -'CgwKx4Rz0mkDABCEyAyRiFgMEhOqgaJiOgBYXGDIB4AMjY20iwvoMsAFXdx1IIQgBCGIxQEUkICD' + -'E2544g1PuMEJOkWlDgIAAAAA4AAAHgAAkg0gIiKaOY4Ojw+QEJERkhKTE5QAAAAAALABgA8AgCQF' + -'iIiIZo6jw+MDJERkhKTE5AQlAAAAAAAAAAAACAgIAAAAAAAEAAAACAhPZ2dTAAQYOwAAAAAAAOGp' + -'bkoCAAAAmc74DRgyNjM69TAzOTk74dnLubewsbagmZiNp4d0KbsExSY/I3XUTwJgkeZdn1HY4zoj' + -'33/q9DFtv3Ui1/jmx7lCUtPt18/sYf9MkgAsAGRBd3gMGP4sU+qCPYBy9VrA3YqJosW3W2/ef1iO' + -'/u3cg8ZG/57jU+pPmbGEJUgkfnaI39DbPqxddZphbMRmCc5rKlkUMkyx8iIoug5dJv1OYH9a59c+' + -'3Gevqc7Z2XFdDjL/qHztRfjWEWxJ/aiGezjohu9HsCZdQBKbiH0VtU/3m85lDG2T/+xkZcYnX+E+' + -'aqzv/xTgOoTFG+x7SNqQ4N+oAABSxuVXw77Jd5bmmTmuJakX7509HH0kGYKvARPpwfOSAPySPAc2' + -'EkneDwB2HwAAJlQDYK5586N79GJCjx4+p6aDUd27XSvRyXLJkIC5YZ1jLv5lpOhZTz0s+DmnF1di' + -'ptrnM6UDgIW11Xh8cHTd0/SmbgOAdxcyWwMAAGIrZ3fNSfZbzKiYrK4+tPqtnMVLOeWOG2kVvUY+' + -'p2PJ/hkCl5aFRO4TLGYPZcIU3vYM1hohS4jHFlnyW/2T5J7kGsShXWT8N05V+3C/GPqJ1QdWisGP' + -'xEzHqXISBPIinWDUt7IeJv/f5OtzBxpTzZZQ+CYEhHXfqG4aABQli72GJhN4oJv+hXcApAJSErAW' + -'8G2raAX4NUcABnVt77CzZAB+LsHcVe+Q4h+QB1wh/ZrJTPxSBdI8mgTeAdTsQOoFUEng9BHcVPhx' + -'SRRYkKWZJXOFYP6V4AEripJoEjXgA2wJRZHSExmJDm8F0A6gEXsg5a4ZsALItrMB7+fh7UKLvYWS' + -'dtsDwFf1mzYzS1F82N1h2Oyt2e76B1QdS0SAsQigLPMOgJS9JRC7hFXA6kUsLFNKD5cA5cTRvgSq' + -'Pc3Fl99xW3QTi/MHR8DEm6WnvaVQATwRqRKjywQ9BrrhugR2AKTsPQeQckrAOgDOhbTESyrXQ50C' + -'kNpXdtWjW7W2/3UjeX3U95gIdalfRAoAmqUEiwp53hCdcCwlg47fcbfzlmQMAgaBkh7c+fcDgF+i' + -'fwDXfzegLPcLYJsAAJQArTXjnh/uXGy3v1Hk3pV6/3t5ruW81f6prfbM2Q3WNVy98BwUtbCwhFhA' + -'WuPev6Oe/4ZaFQUcgKrVs4defzh1TADA1DEh5b3VlDaECw5b+bPfkKos3tIAue3vJZOih3ga3l6O' + -'3PSfIkrLv0PAS86PPdL7g8oc2KteNFKKzKRehOv2gJoFLBPXmaXvPBQILgJon0bbWBszrYZYYwE7' + -'jl2j+vTdU7Vpk21LiU0QajPkywAAHqbUC0/YsYOdb4e6BOp7E0cCi04Ao/TgD8ZVAMid6h/A8IeB' + -'Nkp6/xsAACZELEYIk+yvI6Qz1NN6lIftB/6IMWjWJNOqPTMedAmyaj6Es0QBklJpiSWWHnQ2CoYb' + -'GWAmt+0gLQBFKCBnp2QUUQZ/1thtZDBJUpFWY82z34ocorB62oX7qB5y0oPAv/foxH25wVmgIHf2' + -'xFOr8leZcBq1Kx3ZvCq9Bga639AxuHuPNL/71YCF4EywJpqHFAX6XF0sjVbuANnvvdLcrufYwOM/' + -'iDa6iA468AYAAB6mNBMXcgTD8HSRqJ4vw8CjAlCEPACASlX/APwPOJKl9xQAAAPmnev2eWp33Xgy' + -'w3Dvfz6myGk3oyP8YTKsCOvzAgALQi0o1c6Nzs2O2Pg2h4ACIJAgAGP0aNn5x0BDgVfH7u2TtyfD' + -'cRIuYAyQhBF/lvSRAttgA6TPbWZA9gaUrZWAUEAA+Dx47Q3/r87HxUUqZmB0BmUuMlojFjHt1gDu' + -'nnvuX8MImsjSq5WkzSzGS62OEIlOufWWezxWpv6FBgDgJVltfXFYtNAAnqU0xQoD0YLiXo5cF5QV' + -'4CnY1tBLAkZCOABAhbk/AM+/AwSCCdlWAAAMcFjS7owb8GVDzveDiZvznbt2tF4bL5odN1YKl88T' + -'AEABCZvufq9YCTBtMwVAQUEAwGtNltzSaHvADYC3TxLVjqiRA+OZAMhzcqEgRcAOwoCgvdTxsTHL' + -'QEF6+oOb2+PAI8ciPQcXg7pOY+LjxQSv2fjmFuj34gGwz310/bGK6z3xgT887eomWULEaDd04wHe' + -'tYxdjcgV2SxvSwn0VoZXJRqkRC5ASQ/muVoAUsX7AgAQMBNaVwAAlABRxT/1PmfqLqSRNDbhXb07' + -'berpB3b94jpuWEZjBCD2OcdXFpCKEgCDfcFPMw8AAADUwT4lnUm50lmwrpMMhPQIKj6u0E8fr2vG' + -'BngMNdIlrZsigjahljud6AFVg+tzXwUnXL3TJLpajaWKA4VAAAAMiFfqJgKAZ08XrtS3dxtQNYcp' + -'PvYEG8ClvrQRJgBephwnNWJjtGqmp6VEPSvBe7EBiU3qgJbQAwD4Le8LAMDMhHbNAAAlgK+tFs5O' + -'+YyJc9yCnJa3rxLPulGnxwsXV9Fsk2k4PisCAHC8FkwbGE9gJQAAoMnyksj0CdFMZLLgoz8M+Fxz' + -'iwYBgIx+zHiCBAKAlBKNpF1sO9JpVcyEi9ar15YlHgrut5fPJnkdJ6vEwZPyAHQBIEDUrlMcBAAd' + -'2KAS0Qq+JwRsE4AJZtMnAD6GnOYwYlOIZvtzUNdjreB7fiMkWI0CmBB6AIAKc38A9osEFlTSGECB' + -'+cbeRDC0aRpLHqNPplcK/76Lxn2rpmqyXsYJWRi/FQAAAKBQk9MCAOibrQBQADCDsqpooPutd+05' + -'Ce9g6iEdiYXgVmQAI4+4wskEBEiBloNQ6Ki0/KTQ0QjWfjxzi+AeuXKoMjEVfQOZzr0y941qLgM2' + -'AExvbZOqcxZ6J6krlrj4y2j9AdgKDx6GnJsVLhbc42uq584+ouSdNBpoCiCVHrz+WzUA/DDtD8AT' + -'gA3h0lMCAAzcFv+S+fSSNkeYWlTpb34mf2RfmqqJeMeklhHAfu7VoAEACgAApKRktL+KkQDWMwYC' + -'UAAAAHCKsp80xhp91UjqQBw3x45cetqkjQEyu3G9B6N+R650Uq8OVig7wOm6Wun0ea4lKDPoabJs' + -'6aLqgbhPzpv4KR4iODilw88ZpY7q1IOMcbASAOAVtmcCnobcrkG4KGS7/ZnskVWRNF9J0RUHKOnB' + -'yy9WA8Dv6L4AAARMCQUA4GritfVM2lcZfH3Q3T/vZ47J2YHhcmBazjfdyuV25gLAzrc0cwAAAAAY' + -'Ch6PdwAAAGyWjFW4yScjaWa2mGcofHxWxewKALglWBpLUvwwk+UOh5eNGyUOs1/EF+pZr+ud5Ozo' + -'GwYdAABg2p52LiSgAY/ZVlOmilEgHn6G3OcwYjzI7vOj1t6xsx4S3lBY96EUQBF6AIBAmPYH4PoG' + -'YCoJAADWe+OZJZi7/x76/yH7Lzf9M5XzRKnFPmveMsilQHwVAAAAAKB3LQD8PCIAAADga0QujBLy' + -'wzeJ4a6Z/ERVBAUlAEDqvoM7BQBAuAguzFqILtmjH3Kd4wfKobnOhA3z85qWoRPm9hwoOHoDAAlC' + -'bwDAA56FHAuXflHo3fe2ttG9XUDeA9YmYCBQ0oPr/1QC8IvuCwAAApbUAQCK22MmE3O78VAbHQT9' + -'PIPNoT9zNc3l2Oe7TAVLANBufT8MAQAAAGzT4PS8AQAAoELGHb2uaCwwEv1EWhFriUkbAaAZ27/f' + -'VZnTZXbWz3BwWpjUaMZKRj7dZ0J//gUeTdpVEwAAZOFsNxKAjQSgA+ABPoY8Jj5y2wje81jsXc/1' + -'TOQWTDYZBmAkNDiqVwuA2NJ9AQAAEBKAt9Vrsfs/2N19MO91S9rd8EHTZHnzC5MYmfQEACy/FBcA' + -'AADA5c4gi4z8RANs/m6FNXVo9DV46JG1BBDukqlw/Va5G7QbuGVSI+2aZaoLXJrdVj2zlC9Z5QEA' + -'EFz/5QzgVZwAAAAA/oXcxyC6WfTu+09Ve/c766J4VTAGUFmA51+VANKi/QPoPwYgYAkA715OH4S0' + -'s5KDHvj99MMq8TPFc3roKZnGOoT1bmIhVgc7XAMBAAAAAMAW1VbQw3gapzOpJd+Kd2fc4iSO62fJ' + -'v9+movui1wUNPAj059N3OVxzk4gV73PmE8FIA2F5mRq37Evc76vLXfF4rD5UJJAw46hW6LZCb5sN' + -'Ldx+kzMCAAB+hfy95+965ZCLP7B3/VlTHCvDEKtQhTm4KiCgAEAbrfbWTPssAAAAXpee1tVrozYY' + -'n41wD1aeYtkKfswN5/SXPO0JDnhO/4laUortv/s412fybe/nONdncoCHnBVliu0CQGBWlPY/5Kwo' + -'m2L/kruPM6Q7oz4tvDQy+bZ3HzOi+gNHA4DZEgA=' -); - -lib.resource.add('hterm/images/icon-96', 'image/png;base64', -'iVBORw0KGgoAAAANSUhEUgAAAGAAAABgCAYAAADimHc4AAAStklEQVR42u1dBXjrupL+RzIGmjIf' + -'vAcu42NmZub3lpmZmZmZmRkuMzPDYaYyJG0Sa9b2p2z1eQtp7bzefpv/nKnkkSw7Gg1IshNsDtpo' + -'o4022mijDWp/tlTgzbpJSqYvMoFTC9vjRD5JLb9RYaRkpk22SS28P8pacAaPdZ41KYMCI89YB6wN' + -'3JzQJM3UIGqurfTlKQTAZtqENid5SlNdU804VmbbWQtA6HMkAAdADsBeAJ7mxwIhIhFSXJ9iRPw4' + -'JYDEcqmGWEp1HhCI8gAtpXF7scB1ZRH9E3HObANCNy1AoGTegNDnCdE41tfQDH2t+CINQEpJ9Xp9' + -'7oUDh3+nXK48DYAMIWQmANIkNTn6vP69e3d/zctfeu0nXNexmVn3F0gDAMxMlBoHuht0qnsEEekC' + -'42SdGHmNxgVjgk4bPN04Yui8bhc534cQBH35RKrPN9sGdLnB1/Wuv+HW4f+6/tZvBHAaAJvmKr0A' + -'jJGvyQMw8pLrrvqeT378Ax8UwrKeevoFgEhfjcGGO2JO+iuTt1SW5DHzyraDExyTlWwHjCQ/CAJc' + -'ecU+XHn5xWDmVCGQFAKljsLbx8Ynvv3Bhx7/EQCzurimU04jADLsvK3r73/7W1//g1/6hU++uVqt' + -'0X/dcBcKxRIsy9Ji34DPow2et6FzgcXFKk6fOY83vu4VEFKkDiYHB3roSz73sc+Oj08eOHzk+B9o' + -'MyQABGk0gCIyOt9xHPvaD3/wnT/5VV/+meumpmbwD/98A0qdvVEBNhvMDCJaVXtM01GtVlEs+LBt' + -'C1ngzW98tX/m7Llv/emf+83HarX6vbrfGECQRgBmlLP9Ix961499+zd/5XVj45P407/8FxQ7uiGl' + -'QK1Ww1ZCvR6gXq3AsgQ8zwYzUkMIgXe+/Q1Dd9x5/6duv/P+R7QjprQaIHQd/8orLvnCJz/2/pfm' + -'cj7+6rf+DK5XgOu6sT3dQtBawqjW6lhYXIRlSTAjE/T39eLSS/ZeEwqgE8CiYUV4vQIgTULTyFve' + -'9Or3WJZN/3n9HTh3fgrFjhJmZmawFaGUwkJlEffc9xh83wMYqcFg7Noxinw+l9OBikirAabz7eju' + -'6sxJKTE7W4bn5+D7PrYmtI/gAFJasCwb4IzaBMHzXE8LgBJC4I1GQRKAa4Xo6upEsZiH53nIRYLe' + -'olDMCIIq+nq70dFRAGckgFKpAD+UgBaAgfRRkGvbliwUcoh8ABHFYSfWMnBrxOzL12PwKufzSvV5' + -'5Tpmi5a0IASBQCgWcujs7ABn5AQic+b5rhNlAVAmTliTEwnA990wIxEEdUQYnxjHidMnAUIcBYAB' + -'RqNDdC7BM8t0VtfTnGRd8FKdRIjJcVlCsAbPPA5UAK4rXLJjP7aNbkO9XoPrOrEQWHEm69Kua0ca' + -'YEspvCBQ5toSp9EASCkt27ZF1PlCxBOZOPo5feY0Xpg8jHe/7V3YNjhqjDRac3mMVl1Oo40vtREt' + -'W+2FYwdw/S03YHJ6EkODQ1hcXIQUcaeBlUIWsCwZ+QDLdZxcubKAtBpgNmzZliUa6yLMKiRGoBR2' + -'79yN6666FlJYABgvRhAIncUSHn/iCdQrAZjjSAiKFQQRVEhZIRJASJEACICmlAKQUtqhBETjw5ij' + -'uFqr4oWjBwHmF7/jVUHc6aRNXxAoZA3PdYXruvlldJfTaIATaQA4KU/CzNwMDp84DOYXf+hZXiij' + -'hJz+DK0QAEd+RYTOOAcgMw0g24oskNYAIoCXxDpbnsOxM8fB5qacwKZD+3WQcS+VxQrYYXNVNGMh' + -'I1odiIRQSHb8BmbCpgZYjmVLYi0ANmxQNKpOj50FFOB3WnDzEpOnFkGbuOXPimG5Ap0jLqZOLiKo' + -'MyIsVhfB9lLEpFSQ+S26jh2Fo/n0YagRCUlLRhpAAIMIyWl9vBinAkbfoIPXf+0wnrlxAs/dPInK' + -'VB1CUOsFkdhD6Nnp49oP98EvWfjvnzqGak0hVlwwFJsaoADK9vq2Y0eOOKUGJLTAjjQgFgBAy/gT' + -'vbGIyXC0nX66jJd+YgC7X1nCo39/AccfmUVQU1F5y0d9rsvGJW/txuXv7oGqMx7+2/OoVxWIzE5S' + -'OkfaBBGyhGPHc4G8YYjT+wDLDgUgJbQPWDGuL0/VcefvnMLRB2dw3Uf78dZv345D90zjsX++gPGj' + -'C7peC8yNI7DjpSVcE476rlEPB++awmP/dCEaEMtqbAP1Fqzkhn0VaUAegMzABJkaIMG8epNEiE3R' + -'0funce75Mi4NR+MV7+3B6NUFPPnvY3jupslISJkKoW9PDld/sA+7Xt6B8SMV3Pjzx3Di0TkENQaJ' + -'5A1qM8VRljKPgpg58pcNHyCz0ADSTnhNDTBBglCZruPhvz+PY4/M4Jqwg6772AB2vqwDd/zmKYwd' + -'WQAJpMalb+vGSz81AA6Ah/76HJ69KfI7tej6K7RPUKwaWQT1FmiAlJEJykXZZh5cE02FoaEJkpYE' + -'wGsKwNQGAnDhQAUP/915TJ5YwPCleZSG3WwWvwgYvryAYr8Tm5wn/2Mc5cm481c9RzXWobQPyBpS' + -'ikgDGgJAVvMARzY0AARwc7Y5Ckn3vK4TV7+/D5YncN+fnsWpJ+cgsnDICnj0n85DSOCSUBO6Rl08' + -'8g8XcObZ+VgjSKweKRG1xgcIEQnA9QE46aMgwwlHAmBuOFFepeMRd8rI1cU4FBzYn8exh2bw6D9e' + -'wNihCjgrR0wI21vAzb9yIrT/pfha7/y+nXj+5gk8EWrDzJlF/WxQUgMUwEtREGW/5RlpgJdaABq0' + -'pAGicYFVFaBzxMGV7+vFvtd3YfpsFbf+6ok4KqovxqFoph+YBBAsMg7cPonTT83jsnd247J39IQR' + -'UUcceR28cxrVcrBUX2sAa1Nar7dCAwhevCkDN7UADB9gSyEBaBVYYeT37PTw9u/aAbcg8Pi/XMAz' + -'109gfqLhFAktgX46LbrOg395DscemAnD0X68+suGQ+3L4Y7fOhVHRA00nDBRa3wAEGuAA8DbqABI' + -'kyEA2xFSrBHHM2xf4Ozz82HIOb5kbgSh1TDv69wLZdz0S8dxUTgRHLwkD2HRkgCIdBi6NBPmVpgg' + -'L7krBkrnA6xIA0Qjfl4x9Bw7XInDzHo1hblJbZYoNkvP3zqFw/fPIKgqGNC7aNoEtUQDEJkg23Ec' + -'v1qtrhkFiWYeTYzCUCEEeI15QDTSgjpnMerTmyUB1CsKrGACyvABQb1VAnAt13V8NAHRxGqotEMI' + -'QUbJFgGtMhNuqQa4Ui9HbEgDKFknioKIhC4kbGUwFBhsOGHO/AqhCxAh5dOsBZFBMoqCGhpARJv7' + -'ihul35oEt84E6U0ZCv1APp0T1tACsIhEpquZQhJsT2C9UAGjtqA2vDnPzOD/NUEqymcOJ94TcPJZ' + -'zYSFHYKIjHlA+iXk/kvyeO1XDENYtK6J16kn53H375+OBbFukBkFtWoewHAdJ1qQKwAQWcyEtQaQ' + -'4QPSmk6KZ6gXDlVAcn0x9vTpxTSjdhkBcOYmSO+KNTZlKK0GWHYoASJkZoJIABPHFnDbb5zEFxts' + -'hqEtMkG2rfcEtAZsJAoimBpgGRqg062KVmsAmBH2V2NfWKZ1woxYAyIBwFABXma+nE30wytV4rU/' + -'OK9xLWaGUmpJAHE+awEDUsrGnoCERsooyJYALfPaOEHNByBl7BGwKQsy8kYLUZ1kOTXyZprgUYJH' + -'SBzrctLHDZ6huflCLt61qtWDWAMawsgOWgCe5+v+JYN4vT6AtAbIpSCIGuEcRoaG8TrXRcwzCeZ7' + -'u2gcm4QIZn0QEudC5wGYdYxUt2PyjRSAyWsc6mvW6hW0CnpXzAdgQ6NZAdByJsgKBQAQGCp+oQFQ' + -'8ePdhUIBxWJxXfrJYKQHNRUMMK9kuwhzc3O4eO+eeLQqpbLfFfMaAgAnhdDccrSpAZYtAUApxujI' + -'EN725lfg3//7bvT19cOyLJhg44/ZCTo1y40yI79qmT4/5un2jTx0+XLtmAOAlUJXVx6ve83LdFkr' + -'dsWMTZkUTpikjFyAJUxHFr6oDc918cDDT6KyMB8xzVFpmBpAGGZHiCgVZgoRphSlQkCQTvXxEhFk' + -'lMolXnyseY28NMtlIjXaCzsHO7aPoFDIQ6nWCMDzXS2AdJvybMl4HiaSLyK89S2vxRte/wrU6vXG' + -'IFrzOxdWTZcaMNtCgq15a9vNtWyTMjUncwEguSu2ISesO3vp3YDkE2ZSypiyQMO0JO331gTFryoJ' + -'IXylVLrFOCtEpAHmaG5jbQ3Qb8r45XKFN2qCOCJpSUsxi/n5SlOP8rXB0WpoUgC8HgGwQYqI7AMH' + -'j1G9zk2Ea20wgI5iPhqs8dMk6/26GrOyiqharc16nlffvn3EaWtAc/BcBw8+/Ojc+PjkKaMvuWkN' + -'ME+YnZ17+rnnDxweHOi9iCM+gzbLOXLrG8piu46JIO5/4NHD9XpwbEPfEqjJ01R0XecDYcz8lvhF' + -'MSEkwJIBaU76AZA+SsST5oHOmidqvsHQieYk6ya/ucysT/pPon6yLum/5tXN4uV45ocAKHEeWFdQ' + -'YcpKKb4wNnH/xMTUjwGYArBofLHfuhfjeO+eXbu+/ms+946JyWl16NAxWmV80AZGImW+M0z/dxWU' + -'NbvJNQzaqNK4ro13v/NN9C//doP4gz/+mxKAWWNQb2hHzL/s0n1XDfT3W3fe8wRAVmLytCE56HM3' + -'LL/E+bRqb+niFZ9rSvD0nnHzd2Y+M3vs5Ckwc/S9QQMABgGc0cvS9fU8migi0uUDey7asfvQ4eMQ' + -'louuzs74Am0sL4TZQhHHTpzG8FB/qdRR3DU9M/sUgJqmphfjhJaa9H1v9/Ztw/1PPn0QtWoNs7Oz' + -'WBltATiOixMnzuCS/bvtgTBwCQXg6s5fNLdTmnkuSAKww0WrS7q6St7E5Ax6egbWWHpow3EcnDs/' + -'EX8v6fDw4J4XDhzxASwAEOvSAF2Wu2j3jssAQqVSQ6+ULTQ/W3+pQy/dYHauEi9Sbhsd2gGgqB2x' + -'BEDN+gCpy3rCCGjP5OQ0FHO0idGeDTexHRkoxvjEJHZsGxkE0APgnO5TYc6x1hKAIKJtu3dtGzp1' + -'+hyKxY5oB6wpDWibIRenTp3D6OhQl5RyMAiC5w0TRCtpACW+rM8aGR7cPzTYX3ziqQPw/dzmm4gt' + -'YOaYGZ7n4cTJs3jVK67xw++l23723AVtURLhaFIDEuGnG47+S33fo8mpWZQ6XUxPT6ONtfeD7dgR' + -'j6NQyNHQ0MCOUAA2ANmMBpAhhGJo//eFy6lgFsjn823zsw6cnhyHUhw74kcfe8ozfMCKAkjOAYb2' + -'7tk5cubsBTiuF3v35h1w2xwpRmgxZrBj+/AIgA4AY7pfsZYGyIi6uzv3hHOArocefQbMwNTUVFsD' + -'mjdDIUmcDgfv6OhwH4CIjie0gJfVAF3J2bVjWzgB65TnL0ygs7NrnROwthZUqzWcPHUOV1y2txiu' + -'JA/Pzc0/spYJEob5ye/Zs/NiZka5XEVPr4821gfP9xAN3nA9yB4c6Nt+cG5eLvPGDCdNUKNS7769' + -'u3ZGX1NfqwfR+s//C/PDnH5TRq+kxun8fBkdxQJGhgd2Hjx01BBAwgQl7L/I5fyd4RJE3+TUdNjI' + -'PKSc0AJg/T+JxNNnK5Uly3VuterJOpzh3hmts5DWKExy3/j6l2J4eAAjI4PbjG9UF6YQrMaBWRCu' + -'fu4fHRn0Bvp7USzkUS4vmD9as+IP3cSHWL5eXGTUizk6v/IDubodM7+++qs+ENbsg2RxLlE/5pr1' + -'Ew8H25aFnp6u2CFvGx0e0JHQGdMEJTWgkTo7d4xe3NfXg1KpiLe86TWg9ONtc3eKuVX3yatei5m1' + -'AIa6pRT9QaCeb2YporBzx7Zd0chnRkgKbaSLsMLZcK6/rzecU53n5TSAEkw/HPkFy86BpJtq3LRB' + -'IK6jq7NDhPOqPi0A0+cuuxq6EMas5bGJaVQWFWgTbrqVTdEX9f4ZvmfB9/3Il5bW2hNmnZbDB4om' + -'Lpw/h7n5RYCa+3E0ToY4Jp9XiGSYk/WMvHmlxDEn7yN5ffN4mTzrM808G+0leJqVbG81njbfjFJH' + -'Hr4no4lZ3fjRT06GoWxQ+eFHn7rTz/1Tv5QSrBQpZrAmfVMaQJyNOXHOPESjztJfs54uxFJWl5q1' + -'zYuZRzD+RzAPEufoJFln2TyMv8axwUheJPGRVSMFEHe4ZckqMy8cOXLin5f7xVUyyPypwhKAHp13' + -'IjJCVW4iHGAz30Q5mmx3I+dwyvbWE36x0ck1AFW9Gb+g06qmWkMQVuLEQEtuVldyjR/vFJqyjxNb' + -'6+mTA6DV96HMvkx0ej2pAZZxoBL5QJ8oDKIW3jxnfA5twj1xUhPMjjd9wGpOOEgIgUzaxFG8RZ4F' + -'Tgxos9N1atajtd+S1LytA26p8NKbQE7/0+BtpNakNtpoo4022vgf7lRPtKCE39oAAAAASUVORK5C' + -'YII=' -); - -lib.resource.add('hterm/concat/date', 'text/plain', -'Wed, 19 Jun 2019 17:15:51 +0000' -); - -lib.resource.add('hterm/changelog/version', 'text/plain', -'1.85' -); - -lib.resource.add('hterm/changelog/date', 'text/plain', -'2019-06-17' -); - -lib.resource.add('hterm/git/HEAD', 'text/plain', -'530691a7e349cc0038ef14f1eb2a7c6ce8971e73' -); - -// This file was generated by libdot/bin/concat.sh. -// It has been marked read-only for your safety. Rather than -// edit it directly, please modify one of these source files. -// -// hterm/js/hterm.js -// hterm/js/hterm_accessibility_reader.js -// hterm/js/hterm_contextmenu.js -// hterm/js/hterm_frame.js -// hterm/js/hterm_keyboard.js -// hterm/js/hterm_keyboard_bindings.js -// hterm/js/hterm_keyboard_keymap.js -// hterm/js/hterm_keyboard_keypattern.js -// hterm/js/hterm_options.js -// hterm/js/hterm_parser.js -// hterm/js/hterm_parser_identifiers.js -// hterm/js/hterm_preference_manager.js -// hterm/js/hterm_pubsub.js -// hterm/js/hterm_screen.js -// hterm/js/hterm_scrollport.js -// hterm/js/hterm_terminal.js -// hterm/js/hterm_terminal_io.js -// hterm/js/hterm_text_attributes.js -// hterm/js/hterm_vt.js -// hterm/js/hterm_vt_character_map.js -// hterm/audio/bell.ogg -// hterm/images/icon-96.png - -'use strict'; - -// SOURCE FILE: hterm/js/hterm.js -// Copyright (c) 2012 The Chromium OS 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 Declares the hterm.* namespace and some basic shared utilities - * that are too small to deserve dedicated files. - */ -var hterm = {}; +/** + * @fileoverview Declares the hterm.* namespace and some basic shared utilities + * that are too small to deserve dedicated files. + */ +const hterm = {}; /** * The type of window hosting hterm. @@ -4761,25 +5192,6 @@ hterm.windowType = null; */ hterm.os = null; -/** - * Warning message to display in the terminal when browser zoom is enabled. - * - * You can replace it with your own localized message. - */ -hterm.zoomWarningMessage = 'ZOOM != 100%'; - -/** - * Brief overlay message displayed when text is copied to the clipboard. - * - * By default it is the unicode BLACK SCISSORS character, but you can - * replace it with your own localized message. - * - * This is only displayed when the 'enable-clipboard-notice' preference - * is enabled. - */ -hterm.notifyCopyMessage = '\u2702'; - - /** * Text shown in a desktop notification for the terminal * bell. \u226a is a unicode EIGHTH NOTE, %(title) will @@ -4787,106 +5199,83 @@ hterm.notifyCopyMessage = '\u2702'; */ hterm.desktopNotificationTitle = '\u266A %(title) \u266A'; -/** - * The hterm init function, registered with lib.registerInit(). - * - * This is called during lib.init(). - * - * @param {function} onInit The function lib.init() wants us to invoke when - * initialization is complete. - */ -lib.registerInit('hterm', function(onInit) { - function initOs(os) { - hterm.os = os; - - onInit(); - } - - function initMessageManager() { - lib.i18n.getAcceptLanguages((languages) => { - if (!hterm.messageManager) - hterm.messageManager = new lib.MessageManager(languages); - - // If OS detection fails, then we'll still set the value to something. - // The OS logic in hterm tends to be best effort anyways. - lib.f.getOs().then(initOs).catch(initOs); - }); - } - - function onWindow(window) { - hterm.windowType = window.type; - initMessageManager(); - } - - function onTab(tab) { - if (tab && window.chrome) { - chrome.windows.get(tab.windowId, null, onWindow); - } else { - // TODO(rginda): This is where we end up for a v1 app's background page. - // Maybe windowType = 'none' would be more appropriate, or something. - hterm.windowType = 'normal'; - initMessageManager(); - } - } - - if (!hterm.defaultStorage) { - if (window.chrome && chrome.storage && chrome.storage.sync) { - hterm.defaultStorage = new lib.Storage.Chrome(chrome.storage.sync); - } else { - hterm.defaultStorage = new lib.Storage.Local(); - } - } - - // The chrome.tabs API is not supported in packaged apps, and detecting if - // you're a packaged app is a little awkward. - var isPackagedApp = false; - if (window.chrome && chrome.runtime && chrome.runtime.getManifest) { - var manifest = chrome.runtime.getManifest(); - isPackagedApp = manifest.app && manifest.app.background; - } +/** @type {?lib.MessageManager} */ +hterm.messageManager = null; + +lib.registerInit( + 'hterm', + /** + * The hterm init function, registered with lib.registerInit(). + * + * This is called during lib.init(). + * + * @return {!Promise} + */ + async () => { + function initMessageManager() { + return lib.i18n.getAcceptLanguages() + .then((languages) => { + if (!hterm.messageManager) { + hterm.messageManager = new lib.MessageManager(languages); + } + }) + .then(() => { + // If OS detection fails, then we'll still set the value to + // something. The OS logic in hterm tends to be best effort + // anyways. + const initOs = (os) => { hterm.os = os; }; + return lib.f.getOs().then(initOs).catch(initOs); + }); + } - if (isPackagedApp) { - // Packaged apps are never displayed in browser tabs. - setTimeout(onWindow.bind(null, {type: 'popup'}), 0); - } else { - if (window.chrome && chrome.tabs) { - // The getCurrent method gets the tab that is "currently running", not the - // topmost or focused tab. - chrome.tabs.getCurrent(onTab); - } else { - setTimeout(onWindow.bind(null, {type: 'normal'}), 0); - } - } -}); + function onWindow(window) { + hterm.windowType = window.type; + return initMessageManager(); + } -/** - * Return decimal { width, height } for a given dom node. - */ -hterm.getClientSize = function(dom) { - return dom.getBoundingClientRect(); -}; + function onTab(tab = undefined) { + if (tab && window.chrome) { + return new Promise((resolve) => { + chrome.windows.get(tab.windowId, null, (win) => { + onWindow(win).then(resolve); + }); + }); + } else { + // TODO(rginda): This is where we end up for a v1 app's background + // page. Maybe windowType = 'none' would be more appropriate, or + // something. + hterm.windowType = 'normal'; + return initMessageManager(); + } + } -/** - * Return decimal width for a given dom node. - */ -hterm.getClientWidth = function(dom) { - return dom.getBoundingClientRect().width; -}; + if (!hterm.defaultStorage) { + if (window.chrome && chrome.storage && chrome.storage.sync) { + hterm.defaultStorage = new lib.Storage.Chrome(chrome.storage.sync); + } else { + hterm.defaultStorage = new lib.Storage.Local(); + } + } -/** - * Return decimal height for a given dom node. - */ -hterm.getClientHeight = function(dom) { - return dom.getBoundingClientRect().height; -}; + return new Promise((resolve) => { + if (window.chrome && chrome.tabs) { + // The getCurrent method gets the tab that is "currently running", + // not the topmost or focused tab. + chrome.tabs.getCurrent((tab) => onTab(tab).then(resolve)); + } else { + onWindow({type: 'normal'}).then(resolve); + } + }); + }); /** * Copy the specified text to the system clipboard. * * We'll create selections on demand based on the content to copy. * - * @param {HTMLDocument} document The document with the selection to copy. + * @param {!Document} document The document with the selection to copy. * @param {string} str The string data to copy out. + * @return {!Promise} */ hterm.copySelectionToClipboard = function(document, str) { // Request permission if need be. @@ -4942,8 +5331,7 @@ hterm.copySelectionToClipboard = function(document, str) { copySource.id = 'hterm:copy-to-clipboard-source'; copySource.textContent = str; copySource.style.cssText = ( - '-webkit-user-select: text;' + - '-moz-user-select: text;' + + 'user-select: text;' + 'position: absolute;' + 'top: -99px'); @@ -4960,7 +5348,9 @@ hterm.copySelectionToClipboard = function(document, str) { // https://bugzilla.mozilla.org/show_bug.cgi?id=1178676 try { selection.selectAllChildren(copySource); - } catch (ex) {} + } catch (ex) { + // FF workaround. + } try { document.execCommand('copy'); @@ -4981,7 +5371,7 @@ hterm.copySelectionToClipboard = function(document, str) { } } - copySource.parentNode.removeChild(copySource); + copySource.remove(); // Since execCommand is synchronous, resolve right away. return Promise.resolve(); @@ -4991,66 +5381,50 @@ hterm.copySelectionToClipboard = function(document, str) { return requestPermission().then(writeClipboard); }; -/** - * Paste the system clipboard into the element with focus. - * - * Note: In Chrome/Firefox app/extension environments, you'll need the - * "clipboardRead" permission. In other environments, this might always - * fail as the browser frequently blocks access for security reasons. - * - * @param {HTMLDocument} The document to paste into. - * @return {boolean} True if the paste succeeded. - */ -hterm.pasteFromClipboard = function(document) { - try { - return document.execCommand('paste'); - } catch (firefoxException) { - // Ignore this. FF 40 and older would incorrectly throw an exception if - // there was an error instead of returning false. - return false; - } -}; - /** * Return a formatted message in the current locale. * * @param {string} name The name of the message to return. - * @param {Array=} args The message arguments, if required. + * @param {!Array=} args The message arguments, if required. * @param {string=} string The default message text. * @return {string} The localized message. */ -hterm.msg = function(name, args = [], string) { +hterm.msg = function(name, args = [], string = '') { return hterm.messageManager.get('HTERM_' + name, args, string); }; /** * Create a new notification. * - * @param {Object} params Various parameters for the notification. - * @param {string} params.title The title (defaults to the window's title). - * @param {string} params.body The message body (main text). + * @param {{title:(string|undefined), body:(string|undefined)}=} params Various + * parameters for the notification. + * title The title (defaults to the window's title). + * body The message body (main text). + * @return {!Notification} */ hterm.notify = function(params) { - var def = (curr, fallback) => curr !== undefined ? curr : fallback; - if (params === undefined || params === null) + const def = (curr, fallback) => curr !== undefined ? curr : fallback; + if (params === undefined || params === null) { params = {}; + } // Merge the user's choices with the default settings. We don't take it // directly in case it was stuffed with excess junk. - var options = { + const options = { 'body': params.body, 'icon': def(params.icon, lib.resource.getDataUrl('hterm/images/icon-96')), }; - var title = def(params.title, window.document.title); - if (!title) + let title = def(params.title, window.document.title); + if (!title) { title = 'hterm'; + } title = lib.f.replaceVars(hterm.desktopNotificationTitle, {'title': title}); - var n = new Notification(title, options); + const n = new Notification(title, options); n.onclick = function() { window.focus(); - this.close(); + n.close(); }; return n; }; @@ -5071,72 +5445,76 @@ hterm.openUrl = function(url) { }; /** - * Constructor for a hterm.Size record. + * Tracks size of the terminal. * * Instances of this class have public read/write members for width and height. - * - * @param {integer} width The width of this record. - * @param {integer} height The height of this record. */ -hterm.Size = function(width, height) { - this.width = width; - this.height = height; -}; +hterm.Size = class { + /** + * @param {number} width The width of this record. + * @param {number} height The height of this record. + */ + constructor(width, height) { + this.width = width; + this.height = height; + } -/** - * Adjust the width and height of this record. - * - * @param {integer} width The new width of this record. - * @param {integer} height The new height of this record. - */ -hterm.Size.prototype.resize = function(width, height) { - this.width = width; - this.height = height; -}; + /** + * Adjust the width and height of this record. + * + * @param {number} width The new width of this record. + * @param {number} height The new height of this record. + */ + resize(width, height) { + this.width = width; + this.height = height; + } -/** - * Return a copy of this record. - * - * @return {hterm.Size} A new hterm.Size instance with the same width and - * height. - */ -hterm.Size.prototype.clone = function() { - return new hterm.Size(this.width, this.height); + /** + * Return a copy of this record. + * + * @return {!hterm.Size} A new hterm.Size instance with the same width and + * height. + */ + clone() { + return new this.constructor(this.width, this.height); + } + + /** + * Set the height and width of this instance based on another hterm.Size. + * + * @param {!hterm.Size} that The object to copy from. + */ + setTo(that) { + this.width = that.width; + this.height = that.height; + } + + /** + * Test if another hterm.Size instance is equal to this one. + * + * @param {!hterm.Size} that The other hterm.Size instance. + * @return {boolean} True if both instances have the same width/height, false + * otherwise. + */ + equals(that) { + return this.width == that.width && this.height == that.height; + } + + /** + * Return a string representation of this instance. + * + * @return {string} A string that identifies the width and height of this + * instance. + * @override + */ + toString() { + return `[hterm.Size: ${this.width}, ${this.height}]`; + } }; /** - * Set the height and width of this instance based on another hterm.Size. - * - * @param {hterm.Size} that The object to copy from. - */ -hterm.Size.prototype.setTo = function(that) { - this.width = that.width; - this.height = that.height; -}; - -/** - * Test if another hterm.Size instance is equal to this one. - * - * @param {hterm.Size} that The other hterm.Size instance. - * @return {boolean} True if both instances have the same width/height, false - * otherwise. - */ -hterm.Size.prototype.equals = function(that) { - return this.width == that.width && this.height == that.height; -}; - -/** - * Return a string representation of this instance. - * - * @return {string} A string that identifies the width and height of this - * instance. - */ -hterm.Size.prototype.toString = function() { - return '[hterm.Size: ' + this.width + ', ' + this.height + ']'; -}; - -/** - * Constructor for a hterm.RowCol record. + * Constructor for a hterm.RowCol record. * * Instances of this class have public read/write members for row and column. * @@ -5147,73 +5525,77 @@ hterm.Size.prototype.toString = function() { * happens normally, but any attempt to print new characters causes a cr/lf * first. * - * @param {integer} row The row of this record. - * @param {integer} column The column of this record. - * @param {boolean} opt_overflow Optional boolean indicating that the RowCol - * has overflowed. */ -hterm.RowCol = function(row, column, opt_overflow) { - this.row = row; - this.column = column; - this.overflow = !!opt_overflow; -}; +hterm.RowCol = class { + /** + * @param {number} row The row of this record. + * @param {number} column The column of this record. + * @param {boolean=} overflow Optional boolean indicating that the RowCol + * has overflowed. + */ + constructor(row, column, overflow = false) { + this.row = row; + this.column = column; + this.overflow = !!overflow; + } -/** - * Adjust the row and column of this record. - * - * @param {integer} row The new row of this record. - * @param {integer} column The new column of this record. - * @param {boolean} opt_overflow Optional boolean indicating that the RowCol - * has overflowed. - */ -hterm.RowCol.prototype.move = function(row, column, opt_overflow) { - this.row = row; - this.column = column; - this.overflow = !!opt_overflow; -}; + /** + * Adjust the row and column of this record. + * + * @param {number} row The new row of this record. + * @param {number} column The new column of this record. + * @param {boolean=} overflow Optional boolean indicating that the RowCol + * has overflowed. + */ + move(row, column, overflow = false) { + this.row = row; + this.column = column; + this.overflow = !!overflow; + } -/** - * Return a copy of this record. - * - * @return {hterm.RowCol} A new hterm.RowCol instance with the same row and - * column. - */ -hterm.RowCol.prototype.clone = function() { - return new hterm.RowCol(this.row, this.column, this.overflow); -}; + /** + * Return a copy of this record. + * + * @return {!hterm.RowCol} A new hterm.RowCol instance with the same row and + * column. + */ + clone() { + return new this.constructor(this.row, this.column, this.overflow); + } -/** - * Set the row and column of this instance based on another hterm.RowCol. - * - * @param {hterm.RowCol} that The object to copy from. - */ -hterm.RowCol.prototype.setTo = function(that) { - this.row = that.row; - this.column = that.column; - this.overflow = that.overflow; -}; + /** + * Set the row and column of this instance based on another hterm.RowCol. + * + * @param {!hterm.RowCol} that The object to copy from. + */ + setTo(that) { + this.row = that.row; + this.column = that.column; + this.overflow = that.overflow; + } -/** - * Test if another hterm.RowCol instance is equal to this one. - * - * @param {hterm.RowCol} that The other hterm.RowCol instance. - * @return {boolean} True if both instances have the same row/column, false - * otherwise. - */ -hterm.RowCol.prototype.equals = function(that) { - return (this.row == that.row && this.column == that.column && - this.overflow == that.overflow); -}; + /** + * Test if another hterm.RowCol instance is equal to this one. + * + * @param {!hterm.RowCol} that The other hterm.RowCol instance. + * @return {boolean} True if both instances have the same row/column, false + * otherwise. + */ + equals(that) { + return (this.row == that.row && this.column == that.column && + this.overflow == that.overflow); + } -/** - * Return a string representation of this instance. - * - * @return {string} A string that identifies the row and column of this - * instance. - */ -hterm.RowCol.prototype.toString = function() { - return ('[hterm.RowCol: ' + this.row + ', ' + this.column + ', ' + - this.overflow + ']'); + /** + * Return a string representation of this instance. + * + * @return {string} A string that identifies the row and column of this + * instance. + * @override + */ + toString() { + return `[hterm.RowCol: ${this.row}, ${this.column}, ${this.overflow}]`; + } }; // SOURCE FILE: hterm/js/hterm_accessibility_reader.js // Copyright 2018 The Chromium OS Authors. All rights reserved. @@ -5230,8 +5612,9 @@ hterm.RowCol.prototype.toString = function() { * row of a large piece of output to the screen as it wouldn't be performant. * But we want the screen reader to read it all out in order. * - * @param {HTMLDivElement} div The div element where the live region should be + * @param {!Element} div The div element where the live region should be * added. + * @constructor */ hterm.AccessibilityReader = function(div) { this.document_ = div.ownerDocument; @@ -5242,7 +5625,7 @@ hterm.AccessibilityReader = function(div) { liveRegion.style.cssText = `position: absolute; width: 0; height: 0; overflow: hidden; - left: 0; top: 0;`; + left: -1000px; top: -1000px;`; div.appendChild(liveRegion); // Whether command output should be rendered for Assistive Technology. @@ -5252,7 +5635,6 @@ hterm.AccessibilityReader = function(div) { // This live element is used for command output. this.liveElement_ = this.document_.createElement('p'); this.liveElement_.setAttribute('aria-live', 'polite'); - this.liveElement_.setAttribute('aria-label', ''); liveRegion.appendChild(this.liveElement_); // This live element is used for speaking out the current screen when @@ -5260,7 +5642,6 @@ hterm.AccessibilityReader = function(div) { // announcements. this.assertiveLiveElement_ = this.document_.createElement('p'); this.assertiveLiveElement_.setAttribute('aria-live', 'assertive'); - this.assertiveLiveElement_.setAttribute('aria-label', ''); liveRegion.appendChild(this.assertiveLiveElement_); // A queue of updates to announce. @@ -5304,8 +5685,8 @@ hterm.AccessibilityReader = function(div) { * aria updates make it to the screen reader. We want this to be short so * there's not a big delay between typing/executing commands and hearing output. * - * @constant - * @type {integer} + * @const + * @type {number} */ hterm.AccessibilityReader.DELAY = 50; @@ -5327,7 +5708,7 @@ hterm.AccessibilityReader.prototype.setAccessibilityEnabled = * Decorate the document where the terminal resides. This is needed * for listening to keystrokes on the screen. * - * @param {HTMLDocument} doc The document where the resides. + * @param {!Document} doc The document where the resides. */ hterm.AccessibilityReader.prototype.decorate = function(doc) { const handlers = ['keydown', 'keypress', 'keyup', 'textInput']; @@ -5343,9 +5724,9 @@ hterm.AccessibilityReader.prototype.decorate = function(doc) { * * @param {string} cursorRowString The text in the row that the cursor is * currently on. - * @param {integer} cursorRow The index of the row that the cursor is currently + * @param {number} cursorRow The index of the row that the cursor is currently * on, including rows in the scrollback buffer. - * @param {integer} cursorColumn The index of the column that the cursor is + * @param {number} cursorColumn The index of the column that the cursor is * currently on. */ hterm.AccessibilityReader.prototype.beforeCursorChange = @@ -5378,9 +5759,9 @@ hterm.AccessibilityReader.prototype.beforeCursorChange = * * @param {string} cursorRowString The text in the row that the cursor is * currently on. - * @param {integer} cursorRow The index of the row that the cursor is currently + * @param {number} cursorRow The index of the row that the cursor is currently * on, including rows in the scrollback buffer. - * @param {integer} cursorColumn The index of the column that the cursor is + * @param {number} cursorColumn The index of the column that the cursor is * currently on. */ hterm.AccessibilityReader.prototype.afterCursorChange = @@ -5479,12 +5860,12 @@ hterm.AccessibilityReader.prototype.assertiveAnnounce = function(str) { // So we slightly change the string to ensure that the attribute change gets // registered. str = str.trim(); - if (str == this.assertiveLiveElement_.getAttribute('aria-label')) { + if (str == this.assertiveLiveElement_.innerText) { str = '\n' + str; } this.clear(); - this.assertiveLiveElement_.setAttribute('aria-label', str); + this.assertiveLiveElement_.innerText = str; }; /** @@ -5498,8 +5879,8 @@ hterm.AccessibilityReader.prototype.newLine = function() { * Clear the live region and any in-flight announcements. */ hterm.AccessibilityReader.prototype.clear = function() { - this.liveElement_.setAttribute('aria-label', ''); - this.assertiveLiveElement_.setAttribute('aria-label', ''); + this.liveElement_.innerText = ''; + this.assertiveLiveElement_.innerText = ''; clearTimeout(this.nextReadTimer_); this.nextReadTimer_ = null; this.queue_ = []; @@ -5526,10 +5907,11 @@ hterm.AccessibilityReader.prototype.clear = function() { * * @param {string} cursorRowString The text in the row that the cursor is * currently on. - * @param {integer} cursorRow The index of the row that the cursor is currently + * @param {number} cursorRow The index of the row that the cursor is currently * on, including rows in the scrollback buffer. - * @param {integer} cursorColumn The index of the column that the cursor is + * @param {number} cursorColumn The index of the column that the cursor is * currently on. + * @return {boolean} Whether anything was announced. */ hterm.AccessibilityReader.prototype.announceAction_ = function(cursorRowString, cursorRow, cursorColumn) { @@ -5539,11 +5921,11 @@ hterm.AccessibilityReader.prototype.announceAction_ = } // The case when the row of text hasn't changed at all. - if (this.lastCursorRowString_ == cursorRowString) { + if (lib.notNull(this.lastCursorRowString_) === cursorRowString) { // Moving the cursor along the line. We check that no significant changes // have been queued. If they have, it may not just be a cursor movement and // it may be better to read those out. - if (this.lastCursorColumn_ != cursorColumn && + if (lib.notNull(this.lastCursorColumn_) !== cursorColumn && this.cursorChangeQueue_.join('').trim() == '') { // Announce the text between the old cursor position and the new one. const start = Math.min(this.lastCursorColumn_, cursorColumn); @@ -5608,179 +5990,996 @@ hterm.AccessibilityReader.prototype.announceAction_ = } } } - return false; + return false; + } + + return false; +}; + +/** + * Add text from queue_ to the live region. + * + */ +hterm.AccessibilityReader.prototype.addToLiveRegion_ = function() { + this.nextReadTimer_ = null; + + let str = this.queue_.join('\n').trim(); + + // If the same string is announced twice, an attribute change won't be + // registered and the screen reader won't know that the string has changed. + // So we slightly change the string to ensure that the attribute change gets + // registered. + if (str == this.liveElement_.innerText) { + str = '\n' + str; + } + + this.liveElement_.innerText = str; + this.queue_ = []; +}; +// SOURCE FILE: hterm/js/hterm_contextmenu.js +// Copyright 2018 The Chromium OS 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 Context menu handling. + */ + +/** + * Manage the context menu usually shown when right clicking. + * + * @constructor + */ +hterm.ContextMenu = function() { + // The document that contains this context menu. + this.document_ = null; + // The generated context menu (i.e. HTML elements). + this.element_ = null; + // The structured menu (i.e. JS objects). + /** @type {!Array} */ + this.menu_ = []; +}; + +/** @typedef {{name:(string|symbol), action:function(!Event)}} */ +hterm.ContextMenu.Item; + +/** + * Constant to add a separator to the context menu. + */ +hterm.ContextMenu.SEPARATOR = Symbol('-'); + +/** + * Bind context menu to a specific document element. + * + * @param {!Document} document The document to use when creating elements. + */ +hterm.ContextMenu.prototype.setDocument = function(document) { + if (this.element_) { + this.element_.remove(); + this.element_ = null; + } + this.document_ = document; + this.regenerate_(); + this.document_.body.appendChild(this.element_); +}; + +/** + * Regenerate the HTML elements based on internal menu state. + */ +hterm.ContextMenu.prototype.regenerate_ = function() { + if (!this.element_) { + this.element_ = this.document_.createElement('menu'); + this.element_.id = 'hterm:context-menu'; + } else { + this.hide(); + } + + // Clear out existing menu entries. + while (this.element_.firstChild) { + this.element_.removeChild(this.element_.firstChild); + } + + this.menu_.forEach(({name, action}) => { + const menuitem = this.document_.createElement('menuitem'); + if (name === hterm.ContextMenu.SEPARATOR) { + menuitem.innerHTML = '
'; + menuitem.className = 'separator'; + } else { + menuitem.innerText = name; + menuitem.addEventListener('mousedown', function(e) { + e.preventDefault(); + action(e); + }); + } + this.element_.appendChild(menuitem); + }); +}; + +/** + * Set all the entries in the context menu. + * + * This is an array of arrays. The first element in the array is the string to + * display while the second element is the function to call. + * + * The first element may also be the SEPARATOR constant to add a separator. + * + * This resets all existing menu entries. + * + * @param {!Array} items The menu entries. + */ +hterm.ContextMenu.prototype.setItems = function(items) { + this.menu_ = items; + this.regenerate_(); +}; + +/** + * Show the context menu. + * + * The event is used to determine where to show the menu. + * + * If no menu entries are defined, then nothing will be shown. + * + * @param {!Event} e The event triggering this display. + * @param {!hterm.Terminal=} terminal The terminal object to get style info + * from. + */ +hterm.ContextMenu.prototype.show = function(e, terminal) { + // If there are no menu entries, then don't try to show anything. + if (this.menu_.length == 0) { + return; + } + + // If we have the terminal, sync the style preferences over. + if (terminal) { + this.element_.style.fontSize = terminal.getFontSize(); + this.element_.style.fontFamily = terminal.getFontFamily(); + } + + this.element_.style.top = `${e.clientY}px`; + this.element_.style.left = `${e.clientX}px`; + const docSize = this.document_.body.getBoundingClientRect(); + + this.element_.style.display = 'block'; + + // We can't calculate sizes until after it's displayed. + const eleSize = this.element_.getBoundingClientRect(); + // Make sure the menu isn't clipped outside of the current element. + const minY = Math.max(0, docSize.height - eleSize.height); + const minX = Math.max(0, docSize.width - eleSize.width); + if (minY < e.clientY) { + this.element_.style.top = `${minY}px`; + } + if (minX < e.clientX) { + this.element_.style.left = `${minX}px`; + } +}; + +/** + * Hide the context menu. + */ +hterm.ContextMenu.prototype.hide = function() { + if (!this.element_) { + return; + } + + this.element_.style.display = 'none'; +}; +// SOURCE FILE: hterm/js/hterm_find_bar.js +// Copyright 2020 The Chromium OS 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 Find bar handling. + */ + +/** + * Manage the find bar. + * + * @param {!hterm.Terminal} terminal + * @constructor + */ +hterm.FindBar = function(terminal) { + /** + * @private {!hterm.Terminal} + * @const + */ + this.terminal_ = terminal; + + /** + * @private {!hterm.ScrollPort} + * @const + */ + this.scrollPort_ = terminal.getScrollPort(); + + /** @private {?Element} */ + this.findBar_ = null; + + /** @private {?Element} */ + this.input_ = null; + + /** + * True if find bar input has focus. + * + * @type {boolean} + */ + this.hasFocus = false; + + /** @private {?Element} */ + this.upArrowButton_ = null; + + /** @private {?Element} */ + this.downArrowButton_ = null; + + /** @private {?Element} */ + this.closeButton_ = null; + + /** @private {?Element} */ + this.counterLabel_ = null; + + /** + * Stores current search results mapping row number to row results. + * Also works as cache for find-rows. + * + * @private {!Object} + */ + this.results_ = {}; + + /** + * Timeout ID of pending find batch to run. + * Null indicates no search in progress. + * + * @private {?number} + */ + this.pendingFind_ = null; + + /** + * Timeout ID of pending redraw. + * Null indicates no redraw is scheduled. + * + * @private {?number} + */ + this.pendingRedraw_ = null; + + /** + * Timeout ID of pending row changes. + * Null indicates no row change is scheduled. + * + * @private {?number} + */ + this.pendingNotifyChanges_ = null; + + /** + * List of rows which are changed on terminal. + * + * @private {!Set} + * @const + */ + this.changedRows_ = new Set(); + + /** + * Lower case of find input field. + * + * @private {string} + */ + this.searchText_ = ''; + + /** @private {number} */ + this.batchRow_ = 0; + + /** @private {number} */ + this.batchNum_ = 0; + + /** + * Callbacks to run after the specified batch. Used for testing. + * + * @private {!Object} + * @const + */ + this.batchCallbacksForTest_ = {}; + + /** @type {number} */ + this.batchSize = 500; + + /** + * Findbar is visible or not. + * + * @type {boolean} + */ + this.isVisible = false; + + /** + * Keep track of visible rows. + * + * @private {!Array} + */ + this.visibleRows_ = []; + + /** + * Listens for scroll events and redraws results. + * + * @private {function()} + * @const + */ + this.onScroll_ = this.scheduleRedraw_.bind(this); + + /** + * Row number of selected result. + * + * @private {number} + */ + this.selectedRowNum_ = 0; + + /** + * Index of selected result in its row. + * + * @private {number} + */ + this.selectedRowIndex_ = 0; + + /** + * Index of selected result among all results. + * -1 indicates that there is no current selected result. + * + * @private {number} + */ + this.selectedOrdinal_ = -1; + + /** @private {number} */ + this.resultCount_ = 0; + + /** + * Search match which is currently selected and will have a different + * highlight color. + * + * @private {?Element} + */ + this.selectedResult_ = null; + + /** + * Sorted list of matching row numbers. + * + * @private {!Array} + * @const + */ + this.matchingRowsIndex_ = []; + + /** + * Set to false when the line with the current selected result is + * changed by the terminal and there is no longer a match on that line. + * This will be set back to true when the user next selects up or down. + * + * @private {boolean} + */ + this.selectedResultKnown_ = true; +}; + +/** @typedef {{findRow: ?Element, rowResult: !Array}} */ +hterm.FindBar.RowResult; + +/** @typedef {{index: number, highlighter: ?Element}} */ +hterm.FindBar.Result; + +/** + * Add find bar to the terminal. + * + * @param {!Document} document + */ +hterm.FindBar.prototype.decorate = function(document) { + this.findBar_ = document.createElement('div'); + this.findBar_.id = 'hterm:find-bar'; + this.findBar_.setAttribute('aria-hidden', 'true'); + this.findBar_.innerHTML = lib.resource.getData('hterm/html/find_bar'); + + this.input_ = this.findBar_.querySelector('input'); + this.upArrowButton_ = this.findBar_.querySelector('#hterm\\:find-bar-up'); + this.downArrowButton_ = this.findBar_.querySelector('#hterm\\:find-bar-down'); + this.closeButton_ = this.findBar_.querySelector('#hterm\\:find-bar-close'); + this.counterLabel_ = this.findBar_.querySelector('#hterm\\:find-bar-count'); + + // Add aria-label and svg icons. + this.upArrowButton_.innerHTML = lib.resource + .getData('hterm/images/keyboard_arrow_up'); + this.downArrowButton_.innerHTML = lib.resource + .getData('hterm/images/keyboard_arrow_down'); + this.closeButton_.innerHTML = lib.resource.getData('hterm/images/close'); + + this.upArrowButton_.setAttribute('aria-label', hterm.msg('BUTTON_PREVIOUS')); + this.downArrowButton_.setAttribute('aria-label', hterm.msg('BUTTON_NEXT')); + this.input_.setAttribute('aria-label', hterm.msg('BUTTON_FIND')); + this.closeButton_.setAttribute('aria-label', hterm.msg('BUTTON_CLOSE')); + + // Add event listeners to the elements. + const el = (e) => /** @type {!EventListener} */ (e.bind(this)); + this.input_.addEventListener('input', el(this.onInput_)); + this.input_.addEventListener('keydown', el(this.onKeyDown_)); + this.input_.addEventListener('keypress', el(this.onKeyPressed_)); + this.input_.addEventListener('textInput', el(this.onInputText_)); + this.input_.addEventListener('focus', el(() => { this.hasFocus = true; })); + this.input_.addEventListener('blur', el(() => { this.hasFocus = false; })); + this.closeButton_.addEventListener('click', el(this.close)); + this.upArrowButton_.addEventListener('click', el(this.onPrevious_)); + this.downArrowButton_.addEventListener('click', el(this.onNext_)); + + document.body.appendChild(this.findBar_); + + this.resultScreen_ = document.createElement('div'); + this.resultScreen_.id = 'hterm:find-result-screen'; + this.resultScreen_.innerHTML = lib.resource.getData('hterm/html/find_screen'); + this.resultScreen_.style.display = 'none'; + document.body.appendChild(this.resultScreen_); +}; + +/** + * Display find bar. + */ +hterm.FindBar.prototype.display = function() { + this.scrollPort_.subscribe('scroll', this.onScroll_); + + this.findBar_.classList.add('enabled'); + this.findBar_.removeAttribute('aria-hidden'); + this.input_.focus(); + this.resultScreen_.style.display = ''; + this.isVisible = true; + + // Start searching for stored text in findbar. + this.input_.dispatchEvent(new Event('input')); +}; + +/** + * Close find bar. + */ +hterm.FindBar.prototype.close = function() { + // Clear all results of findbar. + this.resultScreen_.style.display = 'none'; + + this.scrollPort_.unsubscribe('scroll', this.onScroll_); + + this.findBar_.classList.remove('enabled'); + this.findBar_.setAttribute('aria-hidden', 'true'); + this.terminal_.focus(); + this.isVisible = false; + + this.stopSearch(); + this.results_ = {}; + this.resultCount_ = 0; +}; + +/** + * Clears any pending find batch. + */ +hterm.FindBar.prototype.stopSearch = function() { + if (this.pendingFind_ !== null) { + clearTimeout(this.pendingFind_); + this.pendingFind_ = null; + } + this.runBatchCallbackForTest_(0); +}; + +/** + * Enable batch-wise searching when search text changes. + */ +hterm.FindBar.prototype.syncResults_ = function() { + this.batchRow_ = 0; + this.batchNum_ = 0; + this.results_ = {}; + this.resultCount_ = 0; + this.matchingRowsIndex_.length = 0; + this.redraw_(); + this.updateCounterLabel_(); + + // No input means no result. Just redraw the results. + if (!this.searchText_) { + return; + } + + const rowCount = this.terminal_.getRowCount(); + const runNextBatch = () => { + const batchEnd = Math.min(this.batchRow_ + this.batchSize, rowCount); + while (this.batchRow_ < batchEnd) { + // Matching rows are pushed in order of searching to keep list sorted. + if (this.findInRow_(this.batchRow_)) { + this.matchingRowsIndex_.push(this.batchRow_); + } + this.batchRow_++; + } + if (this.batchRow_ < rowCount) { + this.pendingFind_ = setTimeout(runNextBatch); + } else { + this.stopSearch(); + } + ++this.batchNum_; + this.runBatchCallbackForTest_(this.batchNum_); + this.updateCounterLabel_(); + }; + runNextBatch(); +}; + +/** + * Find the results for a particular row and set them in result map. + * TODO(crbug.com/209178): Add support for overflowed rows. + * + * @param {number} rowNum + * @param {boolean=} update True if row should be updated. + * @return {boolean} True if there is a match. + */ +hterm.FindBar.prototype.findInRow_ = function(rowNum, update = false) { + if (!this.searchText_) { + return false; + } + + const prev = this.results_[rowNum]; + if (prev && !update) { + return true; + } + + const rowText = this.terminal_.getRowText(rowNum).toLowerCase(); + const rowResult = []; + + let i; + let startIndex = 0; + // Find and create highlight for matching texts. + while ((i = rowText.indexOf(this.searchText_, startIndex)) != -1) { + rowResult.push({index: i, highlighter: null}); + startIndex = i + this.searchText_.length; + } + + if (rowResult.length) { + this.results_[rowNum] = {findRow: null, rowResult}; + if (this.resultCount_ === 0) { + this.selectedRowNum_ = rowNum; + this.selectedOrdinal_ = 0; + this.upArrowButton_.classList.add('enabled'); + this.downArrowButton_.classList.add('enabled'); + this.scrollToResult_(); + } + } else { + delete this.results_[rowNum]; + } + + // Update the matchCount. + const diff = rowResult.length - (prev ? prev.rowResult.length : 0); + this.resultCount_ += diff; + if (rowNum < this.selectedRowNum_) { + this.selectedOrdinal_ += diff; + } + + return rowResult.length > 0; +}; + +/** + * @param {!Event} event The event triggered on input in find bar. + */ +hterm.FindBar.prototype.onInput_ = function(event) { + this.searchText_ = event.target.value.toLowerCase(); + + // If a batch is already pending, reset it. + clearTimeout(this.pendingFind_); + this.pendingFind_ = setTimeout(() => this.syncResults_()); +}; + +/** + * @param {!Event} event The event triggered on key press in find bar. + */ +hterm.FindBar.prototype.onKeyPressed_ = function(event) { + event.stopPropagation(); +}; + +/** + * @param {!Event} event The event triggered on text input in find bar. + */ +hterm.FindBar.prototype.onInputText_ = function(event) { + event.stopPropagation(); +}; + +/** + * @param {!Event} event The event triggered on keydown in find bar. + */ +hterm.FindBar.prototype.onKeyDown_ = function(event) { + if (event.metaKey || event.altKey) { + event.stopPropagation(); + return; + } + if (event.key == 'Escape') { + this.close(); + } + if (event.key == 'Enter') { + if (event.shiftKey) { + this.onPrevious_(); + } else { + this.onNext_(); + } } - - return false; + // keyCode for G. + if (event.ctrlKey && event.keyCode == 71) { + if (event.shiftKey) { + this.onPrevious_(); + } else { + this.onNext_(); + } + event.preventDefault(); + } + // Stop Ctrl+F inside hterm find input opening browser find. + // keyCode for F. + if (event.ctrlKey && event.keyCode == 70) { + event.preventDefault(); + } + event.stopPropagation(); }; /** - * Add text from queue_ to the live region. + * Set the background color to highlight find results. * + * @param {string=} color The color to set. If not defined, we reset to the + * saved user preference. */ -hterm.AccessibilityReader.prototype.addToLiveRegion_ = function() { - this.nextReadTimer_ = null; +hterm.FindBar.prototype.setFindResultColor = function(color) { + if (color === undefined) { + color = this.terminal_.getPrefs().getString('find-result-color'); + } - let str = this.queue_.join('\n').trim(); + this.terminal_.setCssVar('find-result-color', color); +}; - // If the same string is announced twice, an attribute change won't be - // registered and the screen reader won't know that the string has changed. - // So we slightly change the string to ensure that the attribute change gets - // registered. - if (str == this.liveElement_.getAttribute('aria-label')) { - str = '\n' + str; +/** + * Set the background color to highlight the selected find result. + * + * @param {string=} color The color to set. If not defined, we reset to the + * saved user preference. + */ +hterm.FindBar.prototype.setFindResultSelectedColor = + function(color = undefined) { + if (color === undefined) { + color = this.terminal_.getPrefs().getString('find-result-selected-color'); } - this.liveElement_.setAttribute('aria-label', str); - this.queue_ = []; + this.terminal_.setCssVar('find-result-selected-color', color); }; -// SOURCE FILE: hterm/js/hterm_contextmenu.js -// Copyright 2018 The Chromium OS 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 Context menu handling. + * Register a callback to be run after the specified batch (1-based). + * Use batchNum 0 to set a callback to be run when search stops. + * Used for testing. + * + * @param {number} batchNum + * @param {function()} callback */ +hterm.FindBar.prototype.setBatchCallbackForTest = function(batchNum, callback) { + this.batchCallbacksForTest_[batchNum] = callback; +}; /** - * Manage the context menu usually shown when right clicking. + * Redraws the results of findbar on find result screen. */ -hterm.ContextMenu = function() { - // The document that contains this context menu. - this.document_ = null; - // The generated context menu (i.e. HTML elements). - this.element_ = null; - // The structured menu (i.e. JS objects). - this.menu_ = []; +hterm.FindBar.prototype.redraw_ = function() { + const topRowIndex = this.scrollPort_.getTopRowIndex(); + const bottomRowIndex = this.scrollPort_.getBottomRowIndex(topRowIndex); + + // Clear the find result screen. + this.visibleRows_.forEach((row) => { + row.remove(); + }); + this.visibleRows_ = []; + + for (let rowNum = topRowIndex; rowNum <= bottomRowIndex; rowNum++) { + const newRow = this.fetchRowNode_(rowNum); + this.resultScreen_.appendChild(newRow); + this.visibleRows_.push(newRow); + } + + delete this.pendingRedraw_; + this.highlightSelectedResult_(); }; /** - * Constant to add a separator to the context menu. + * Fetch find row element. If find-row is not available in results, it creates + * a new one and store it in results. + * + * @param {number} rowNum + * @return {!Element} */ -hterm.ContextMenu.SEPARATOR = {}; +hterm.FindBar.prototype.fetchRowNode_ = function(rowNum) { + // Process row if batch hasn't yet got to it. + if (rowNum >= this.batchRow_) { + this.findInRow_(rowNum); + } + const row = this.results_[rowNum]; + if (row && row.findRow) { + return row.findRow; + } + + // Create a new find-row. + const findRow = this.terminal_.getDocument().createElement('find-row'); + if (!row) { + return findRow; + } + row.rowResult.forEach((result) => { + const highlighter = this.terminal_.getDocument().createElement('div'); + highlighter.classList.add('find-highlighter'); + highlighter.style.left = + `calc(var(--hterm-charsize-width) * ${result.index})`; + highlighter.style.width = + `calc(var(--hterm-charsize-width) * ${this.searchText_.length})`; + result.highlighter = highlighter; + findRow.appendChild(highlighter); + }); + return row.findRow = findRow; +}; /** - * Bind context menu to a specific document element. + * Synchronize redrawing of search results present on the screen. * - * @param {HTMLDocument} document The document to use when creating elements. + * The sync will happen asynchronously, soon after the call stack winds down. + * Multiple calls will be coalesced into a single sync. */ -hterm.ContextMenu.prototype.setDocument = function(document) { - if (this.element_) { - this.element_.remove(); - this.element_ = null; +hterm.FindBar.prototype.scheduleRedraw_ = function() { + if (this.pendingRedraw_) { + return; } - this.document_ = document; - this.regenerate_(); - this.document_.body.appendChild(this.element_); + this.pendingRedraw_ = setTimeout(() => { + this.redraw_(); + }); }; /** - * Regenerate the HTML elements based on internal menu state. + * Runs the specified batch callback if it exists and removes it. + * + * @param {number} batchNum + * @private */ -hterm.ContextMenu.prototype.regenerate_ = function() { - if (!this.element_) { - this.element_ = this.document_.createElement('menu'); - this.element_.id = 'hterm:context-menu'; - this.element_.style.cssText = ` - display: none; - border: solid 1px; - position: absolute; - `; - } else { - this.hide(); +hterm.FindBar.prototype.runBatchCallbackForTest_ = function(batchNum) { + const callback = this.batchCallbacksForTest_[batchNum]; + if (callback) { + callback(); + delete this.batchCallbacksForTest_[batchNum]; } +}; - // Clear out existing menu entries. - while (this.element_.firstChild) { - this.element_.removeChild(this.element_.firstChild); +/** + * Update the counterLabel for findbar. + */ +hterm.FindBar.prototype.updateCounterLabel_ = function() { + // Reset the counterLabel if no results are present. + if (this.resultCount_ === 0) { + this.selectedRowNum_ = 0; + this.selectedRowIndex_ = 0; + this.selectedOrdinal_ = -1; + this.selectedResultKnown_ = true; + this.upArrowButton_.classList.remove('enabled'); + this.downArrowButton_.classList.remove('enabled'); + } + // Update the counterLabel. + if (this.selectedResultKnown_) { + this.counterLabel_.textContent = hterm.msg( + 'FIND_COUNTER_LABEL', [this.selectedOrdinal_ + 1, this.resultCount_]); + } else { + this.counterLabel_.textContent = hterm.msg( + 'FIND_RESULT_COUNT', [this.resultCount_]); } + this.highlightSelectedResult_(); +}; - this.menu_.forEach(([name, action]) => { - const menuitem = this.document_.createElement('menuitem'); - if (name === hterm.ContextMenu.SEPARATOR) { - menuitem.innerHTML = '
'; - menuitem.className = 'separator'; +/** + * Returns the largest index of arr with arr[index] <= value, or -1. + * + * @param {!Array} arr Array to be searched + * @param {number} value + * @return {number} + */ +hterm.FindBar.indexOf = function(arr, value) { + let index = -1; + let low = 0; + let high = arr.length - 1; + while (low <= high) { + const mid = Math.floor((low + high) / 2); + if (arr[mid] <= value) { + index = mid; + low = mid + 1; } else { - menuitem.innerText = name; - menuitem.addEventListener('mousedown', function(e) { - e.preventDefault(); - action(e); - }); + high = mid - 1; } - this.element_.appendChild(menuitem); - }); + } + return index; }; /** - * Set all the entries in the context menu. - * - * This is an array of arrays. The first element in the array is the string to - * display while the second element is the function to call. - * - * The first element may also be the SEPARATOR constant to add a separator. - * - * This resets all existing menu entries. + * Returns true if matchingRowsIndex_ index can be used to find next + * via binary search. * - * @param {Array>} items The menu entries. + * @private + * @param {number} step 1 to find next in down direction, -1 to find next in up + * direction + * @return {boolean} */ -hterm.ContextMenu.prototype.setItems = function(items) { - this.menu_ = items; - this.regenerate_(); +hterm.FindBar.prototype.canUseMatchingRowsIndex_ = function(step) { + // We can use the matchingRowsIndex_ index to find next via binary search + // if either all batches are done, or if selectedRowNum_ is within the index. + const topRowIndex = this.scrollPort_.getTopRowIndex(); + const bottomRowIndex = this.scrollPort_.getBottomRowIndex(topRowIndex); + const index = this.matchingRowsIndex_; + const current = this.selectedRowNum_; + + return this.batchRow_ > bottomRowIndex || + (step > 0 && current < index[index.length - 1]) || + (step < 0 && current < this.batchRow_ && current > index[0]); }; /** - * Show the context menu. - * - * The event is used to determine where to show the menu. - * - * If no menu entries are defined, then nothing will be shown. + * Select the next matching row from the current selected row in either up or + * down direction. If batch searching is complete, moving between + * results can be done by finding the adjacent item in matchingRowsIndex_. + * When batching is not yet complete, we will use matchingRowsIndex_ when we + * can, and also do a brute force search across the current visible screen, + * but we will not allow the user to select results that are outside of the + * visible screen, or the index. * - * @param {Event} e The event triggering this display. - * @param {hterm.Terminal=} terminal The terminal object to get style info from. + * @param {number} step 1 to find next in down direction, -1 to find next in up + * direction. */ -hterm.ContextMenu.prototype.show = function(e, terminal) { - // If there are no menu entries, then don't try to show anything. - if (this.menu_.length == 0) { +hterm.FindBar.prototype.selectNext_ = function(step) { + // Increment/decrement i by s modulo len. + const circularStep = (i, s, len) => (i + s + len) % len; + const stepOnce = (prev, next) => step > 0 ? prev : next; + + const row = this.results_[this.selectedRowNum_]; + if (row && row.rowResult[this.selectedRowIndex_ + step] !== undefined) { + // Move to another match on the same row. + this.selectedRowIndex_ += step; + } else { + let topRowIndex = this.scrollPort_.getTopRowIndex(); + const bottomRowIndex = this.scrollPort_.getBottomRowIndex(topRowIndex); + const index = this.matchingRowsIndex_; + const current = this.selectedRowNum_; + + if (this.canUseMatchingRowsIndex_(step)) { + let i = hterm.FindBar.indexOf(index, current); + if (!this.selectedResultKnown_ && step < 0) { + i++; + } + this.selectedRowNum_ = index[circularStep(i, step, index.length)]; + } else { + // Not using the index, so brute force search in visible screen. + let start = current + step; + // If outside visible screen, then move to the boundary, but first adjust + // topRowIndex for if a batch has partially covered the screen. + topRowIndex = Math.max(topRowIndex, this.batchRow_); + if (current < topRowIndex || current > bottomRowIndex) { + start = stepOnce(topRowIndex, bottomRowIndex); + } + const end = stepOnce(bottomRowIndex + 1, topRowIndex - 1); + + // If we don't end up finding anything, use the first or last in index. + // If the index is empty, stay where we are. + if (index.length > 0) { + this.selectedRowNum_ = index[stepOnce(0, index.length - 1)]; + } + for (let i = start; i != end; i += step) { + if (this.results_[i]) { + this.selectedRowNum_ = i; + break; + } + } + } + const row = this.results_[this.selectedRowNum_]; + this.selectedRowIndex_ = stepOnce(0, row.rowResult.length - 1); + } + + // If the previous selected result was deleted and we move down, + // then ordinal stays the same. + const s = !this.selectedResultKnown_ && step > 0 ? 0 : step; + this.selectedOrdinal_ = circularStep( + this.selectedOrdinal_, s, this.resultCount_); + + this.selectedResultKnown_ = true; + this.scrollToResult_(); + this.updateCounterLabel_(); +}; + +/** + * Select the next match. + */ +hterm.FindBar.prototype.onNext_ = function() { + if (!this.downArrowButton_.classList.contains('enabled')) { return; } + this.selectNext_(1); +}; - // If we have the terminal, sync the style preferences over. - if (terminal) { - this.element_.style.backgroundColor = terminal.getBackgroundColor(); - this.element_.style.color = terminal.getForegroundColor(); - this.element_.style.fontSize = terminal.getFontSize(); - this.element_.style.fontFamily = terminal.getFontFamily(); +/** + * Select the previous match. + */ +hterm.FindBar.prototype.onPrevious_ = function() { + if (!this.upArrowButton_.classList.contains('enabled')) { + return; } + this.selectNext_(-1); +}; - this.element_.style.top = `${e.clientY}px`; - this.element_.style.left = `${e.clientX}px`; - const docSize = hterm.getClientSize(this.document_.body); +/** + * Scroll the terminal up/down depending upon the row of selected result. + */ +hterm.FindBar.prototype.scrollToResult_ = function() { + const topRowIndex = this.scrollPort_.getTopRowIndex(); + const bottomRowIndex = this.scrollPort_.getBottomRowIndex(topRowIndex); - this.element_.style.display = 'block'; + if (this.selectedRowNum_ < topRowIndex || + this.selectedRowNum_ > bottomRowIndex) { + this.scrollPort_.scrollRowToMiddle(this.selectedRowNum_); + } +}; - // We can't calculate sizes until after it's displayed. - const eleSize = hterm.getClientSize(this.element_); - // Make sure the menu isn't clipped outside of the current element. - const minY = Math.max(0, docSize.height - eleSize.height); - const minX = Math.max(0, docSize.width - eleSize.width); - if (minY < e.clientY) { - this.element_.style.top = `${minY}px`; +/** + * Sets CSS to highlight selected result. + */ +hterm.FindBar.prototype.highlightSelectedResult_ = function() { + // Remove selected result. + if (this.selectedResult_) { + this.selectedResult_.classList.remove('selected'); + this.selectedResult_ = null; } - if (minX < e.clientX) { - this.element_.style.left = `${minX}px`; + + // Select new instance of result. + if (this.resultCount_ && this.selectedResultKnown_) { + this.selectedResult_ = this.results_[this.selectedRowNum_] + .rowResult[this.selectedRowIndex_].highlighter; + if (this.selectedResult_) { + this.selectedResult_.classList.add('selected'); + } } }; /** - * Hide the context menu. + * Synchronize changing of rows on terminal. + * + * The sync will happen asynchronously, soon after the call stack winds down. + * Multiple calls will be coalesced into a single sync. + * + * @param {number} rowNum */ -hterm.ContextMenu.prototype.hide = function() { - if (!this.element_) { +hterm.FindBar.prototype.scheduleNotifyChanges = function(rowNum) { + if (!this.isVisible) { + return; + } + this.changedRows_.add(rowNum); + if (this.pendingNotifyChanges_) { return; } - this.element_.style.display = 'none'; + this.pendingNotifyChanges_ = setTimeout(() => { + this.notifyChanges_(); + }); +}; + +/** + * Change results of all changed rows. + */ +hterm.FindBar.prototype.notifyChanges_ = function() { + this.changedRows_.forEach((rowNum) => { + rowNum += this.scrollPort_.getTopRowIndex(); + const prev = this.results_[rowNum]; + const found = this.findInRow_(rowNum, true); + if (this.selectedRowNum_ == rowNum) { + // If selected row is modified the first result of the row is selected. + this.selectedOrdinal_ -= this.selectedRowIndex_; + this.selectedRowIndex_ = 0; + this.selectedResultKnown_ = found; + } + + // Update the index if the changed row needs to be added or removed. + if (!!prev !== found) { + const i = hterm.FindBar.indexOf(this.matchingRowsIndex_, rowNum); + if (found) { + this.matchingRowsIndex_.splice(i + 1, 0, rowNum); + } else { + this.matchingRowsIndex_.splice(i, 1); + } + } + }); + + this.updateCounterLabel_(); + this.redraw_(); + this.changedRows_.clear(); + delete this.pendingNotifyChanges_; }; // SOURCE FILE: hterm/js/hterm_frame.js // Copyright (c) 2012 The Chromium OS Authors. All rights reserved. @@ -5800,15 +6999,16 @@ hterm.ContextMenu.prototype.hide = function() { /** * Construct a new frame for the given terminal. * - * @param terminal {hterm.Terminal} The parent terminal object. - * @param url {String} The url to load in the frame. - * @param opt_options {Object} Optional options for the frame. Not implemented. + * @param {!hterm.Terminal} terminal The parent terminal object. + * @param {string} url The url to load in the frame. + * @param {!Object=} options Optional options for the frame. Not implemented. + * @constructor */ -hterm.Frame = function(terminal, url, opt_options) { +hterm.Frame = function(terminal, url, options = {}) { this.terminal_ = terminal; this.div_ = terminal.div_; this.url = url; - this.options = opt_options || {}; + this.options = options; this.iframe_ = null; this.container_ = null; this.messageChannel_ = null; @@ -5816,6 +7016,8 @@ hterm.Frame = function(terminal, url, opt_options) { /** * Handle messages from the iframe. + * + * @param {!MessageEvent} e The message to process. */ hterm.Frame.prototype.onMessage_ = function(e) { switch (e.data.name) { @@ -5834,7 +7036,6 @@ hterm.Frame.prototype.onMessage_ = function(e) { return; default: console.log('Unknown message from frame:', e.data); - return; } }; @@ -5867,18 +7068,17 @@ hterm.Frame.prototype.onLoad = function() {}; * Sends the terminal-info message to the iframe. */ hterm.Frame.prototype.sendTerminalInfo_ = function() { - lib.i18n.getAcceptLanguages(function(languages) { - this.postMessage('terminal-info', [{ - acceptLanguages: languages, - foregroundColor: this.terminal_.getForegroundColor(), - backgroundColor: this.terminal_.getBackgroundColor(), - cursorColor: this.terminal_.getCursorColor(), - fontSize: this.terminal_.getFontSize(), - fontFamily: this.terminal_.getFontFamily(), - baseURL: lib.f.getURL('/') - }] - ); - }.bind(this)); + lib.i18n.getAcceptLanguages().then((languages) => { + this.postMessage('terminal-info', [{ + acceptLanguages: languages, + foregroundColor: this.terminal_.getForegroundColor(), + backgroundColor: this.terminal_.getBackgroundColor(), + cursorColor: this.terminal_.getCursorColor(), + fontSize: this.terminal_.getFontSize(), + fontFamily: this.terminal_.getFontFamily(), + baseURL: lib.f.getURL('/'), + }]); + }); }; /** @@ -5892,10 +7092,11 @@ hterm.Frame.prototype.onCloseClicked_ = function() { * Close this frame. */ hterm.Frame.prototype.close = function() { - if (!this.container_ || !this.container_.parentNode) - return; + if (!this.container_ || !this.container_.parentNode) { + return; + } - this.container_.parentNode.removeChild(this.container_); + this.container_.remove(); this.onClose(); }; @@ -5907,10 +7108,14 @@ hterm.Frame.prototype.onClose = function() {}; /** * Send a message to the iframe. + * + * @param {string} name The message name. + * @param {!Array=} argv The message arguments. */ hterm.Frame.prototype.postMessage = function(name, argv) { - if (!this.messageChannel_) + if (!this.messageChannel_) { throw new Error('Message channel is not set up.'); + } this.messageChannel_.port1.postMessage({name: name, argv: argv}); }; @@ -5921,34 +7126,14 @@ hterm.Frame.prototype.postMessage = function(name, argv) { * The iframe src is not loaded until this method is called. */ hterm.Frame.prototype.show = function() { - var self = this; - - function opt(name, defaultValue) { - if (name in self.options) - return self.options[name]; - - return defaultValue; - } - - var self = this; - if (this.container_ && this.container_.parentNode) { console.error('Frame already visible'); return; } - var headerHeight = '16px'; - - var divSize = hterm.getClientSize(this.div_); + const document = this.terminal_.document_; - var width = opt('width', 640); - var height = opt('height', 480); - var left = (divSize.width - width) / 2; - var top = (divSize.height - height) / 2; - - var document = this.terminal_.document_; - - var container = this.container_ = document.createElement('div'); + const container = this.container_ = document.createElement('div'); container.style.cssText = ( 'position: absolute;' + 'display: none;' + @@ -5962,31 +7147,7 @@ hterm.Frame.prototype.show = function() { 'box-shadow: 0 0 2px ' + this.terminal_.getForegroundColor() + ';' + 'border: 2px ' + this.terminal_.getForegroundColor() + ' solid;'); - if (false) { - // No use for the close button, so no use for the window header either. - var header = document.createElement('div'); - header.style.cssText = ( - 'display: flex;' + - 'justify-content: flex-end;' + - 'height: ' + headerHeight + ';' + - 'background-color: ' + this.terminal_.getForegroundColor() + ';' + - 'color: ' + this.terminal_.getBackgroundColor() + ';' + - 'font-size: 16px;' + - 'font-family: ' + this.terminal_.getFontFamily()); - container.appendChild(header); - - var button = document.createElement('div'); - button.setAttribute('role', 'button'); - button.style.cssText = ( - 'margin-top: -3px;' + - 'margin-right: 3px;' + - 'cursor: pointer;'); - button.textContent = '\u2a2f'; - button.addEventListener('click', this.onCloseClicked_.bind(this)); - header.appendChild(button); - } - - var iframe = this.iframe_ = document.createElement('iframe'); + const iframe = this.iframe_ = document.createElement('iframe'); iframe.onload = this.onLoad_.bind(this); iframe.style.cssText = ( 'display: flex;' + @@ -6011,7 +7172,9 @@ hterm.Frame.prototype.show = function() { * * See also: [XTERM] as referenced in vt.js. * - * @param {hterm.Terminal} The Terminal object associated with this keyboard. + * @param {!hterm.Terminal} terminal The Terminal object associated with this + * keyboard. + * @constructor */ hterm.Keyboard = function(terminal) { // The parent vt interpreter. @@ -6027,7 +7190,7 @@ hterm.Keyboard = function(terminal) { ['keydown', this.onKeyDown_.bind(this)], ['keypress', this.onKeyPress_.bind(this)], ['keyup', this.onKeyUp_.bind(this)], - ['textInput', this.onTextInput_.bind(this)] + ['textInput', this.onTextInput_.bind(this)], ]; /** @@ -6035,10 +7198,10 @@ hterm.Keyboard = function(terminal) { */ this.keyMap = new hterm.Keyboard.KeyMap(this); - this.bindings = new hterm.Keyboard.Bindings(this); + this.bindings = new hterm.Keyboard.Bindings(); /** - * none: Disable any AltGr related munging. + * none: Disable the AltGr emulation. * ctrl-alt: Assume Ctrl+Alt means AltGr. * left-alt: Assume left Alt means AltGr. * right-alt: Assume right Alt means AltGr. @@ -6046,7 +7209,7 @@ hterm.Keyboard = function(terminal) { this.altGrMode = 'none'; /** - * If true, Shift-Insert will fall through to the browser as a paste. + * If true, Shift+Insert will fall through to the browser as a paste. * If false, the keystroke will be sent to the host. */ this.shiftInsertPaste = true; @@ -6064,9 +7227,9 @@ hterm.Keyboard = function(terminal) { this.pageKeysScroll = false; /** - * If true, Ctrl-Plus/Minus/Zero controls zoom. - * If false, Ctrl-Shift-Plus/Minus/Zero controls zoom, Ctrl-Minus sends ^_, - * Ctrl-Plus/Zero do nothing. + * If true, Ctrl+Plus/Minus/Zero controls zoom. + * If false, Ctrl+Shift+Plus/Minus/Zero controls zoom, Ctrl+Minus sends ^_, + * Ctrl+Plus/Zero do nothing. */ this.ctrlPlusMinusZeroZoom = true; @@ -6171,6 +7334,9 @@ hterm.Keyboard = function(terminal) { /** * Special handling for keyCodes in a keyboard layout. + * + * @enum {symbol} + * @const */ hterm.Keyboard.KeyActions = { /** @@ -6219,9 +7385,12 @@ hterm.Keyboard.KeyActions = { * It is useful for a modified key action, where it essentially strips the * modifier while preventing the browser from reacting to the key. */ - STRIP: Symbol('STRIP') + STRIP: Symbol('STRIP'), }; +/** @typedef {string|!hterm.Keyboard.KeyActions} */ +hterm.Keyboard.KeyAction; + /** * Capture keyboard events sent to the associated element. * @@ -6230,18 +7399,20 @@ hterm.Keyboard.KeyActions = { * * Passing a null element will uninstall the keyboard handlers. * - * @param {HTMLElement} element The element whose events should be captured, or + * @param {?Element} element The element whose events should be captured, or * null to disable the keyboard. */ hterm.Keyboard.prototype.installKeyboard = function(element) { - if (element == this.keyboardElement_) + if (element == this.keyboardElement_) { return; + } - if (element && this.keyboardElement_) + if (element && this.keyboardElement_) { this.installKeyboard(null); + } - for (var i = 0; i < this.handlers_.length; i++) { - var handler = this.handlers_[i]; + for (let i = 0; i < this.handlers_.length; i++) { + const handler = this.handlers_[i]; if (element) { element.addEventListener(handler[0], handler[1]); } else { @@ -6262,14 +7433,17 @@ hterm.Keyboard.prototype.uninstallKeyboard = function() { }; /** - * Handle onTextInput events. + * Handle textInput events. * * These are generated when using IMEs, Virtual Keyboards (VKs), compose keys, * Unicode input, etc... + * + * @param {!InputEvent} e The event to process. */ hterm.Keyboard.prototype.onTextInput_ = function(e) { - if (!e.data) + if (!e.data) { return; + } // Just pass the generated buffer straight down. No need for us to split it // up or otherwise parse it ahead of times. @@ -6277,9 +7451,11 @@ hterm.Keyboard.prototype.onTextInput_ = function(e) { }; /** - * Handle onKeyPress events. + * Handle keypress events. * * TODO(vapier): Drop this event entirely and only use keydown. + * + * @param {!KeyboardEvent} e The event to process. */ hterm.Keyboard.prototype.onKeyPress_ = function(e) { // FF doesn't set keyCode reliably in keypress events. Stick to the which @@ -6299,6 +7475,8 @@ hterm.Keyboard.prototype.onKeyPress_ = function(e) { return; } + /** @type {string} */ + let ch; if (e.altKey && this.altSendsWhat == 'browser-key' && e.charCode == 0) { // If we got here because we were expecting the browser to handle an // alt sequence but it didn't do it, then we might be on an OS without @@ -6307,58 +7485,54 @@ hterm.Keyboard.prototype.onKeyPress_ = function(e) { // // This happens here only as a fallback. Typically these platforms should // set altSendsWhat to either 'escape' or '8-bit'. - var ch = String.fromCharCode(e.keyCode); - if (!e.shiftKey) + ch = String.fromCharCode(e.keyCode); + if (!e.shiftKey) { ch = ch.toLowerCase(); + } } else if (e.charCode >= 32) { - ch = e.charCode; + ch = String.fromCharCode(e.charCode); } - if (ch) - this.terminal.onVTKeystroke(String.fromCharCode(ch)); + if (ch) { + this.terminal.onVTKeystroke(ch); + } e.preventDefault(); e.stopPropagation(); }; /** - * Prevent default handling for non-ctrl-shifted event. + * Handle focusout events. * - * When combined with Chrome permission 'app.window.fullscreen.overrideEsc', - * and called for both key down and key up events, - * the ESC key remains usable within fullscreen Chrome app windows. + * @param {!FocusEvent} e The event to process. */ -hterm.Keyboard.prototype.preventChromeAppNonCtrlShiftDefault_ = function(e) { - if (!window.chrome || !window.chrome.app || !window.chrome.app.window) - return; - if (!e.ctrlKey || !e.shiftKey) - e.preventDefault(); -}; - hterm.Keyboard.prototype.onFocusOut_ = function(e) { this.altKeyPressed = 0; }; +/** + * Handle keyup events. + * + * @param {!KeyboardEvent} e The event to process. + */ hterm.Keyboard.prototype.onKeyUp_ = function(e) { - if (e.keyCode == 18) + if (e.keyCode == 18) { this.altKeyPressed = this.altKeyPressed & ~(1 << (e.location - 1)); - - if (e.keyCode == 27) - this.preventChromeAppNonCtrlShiftDefault_(e); + } }; /** - * Handle onKeyDown events. + * Handle keydown events. + * + * @param {!KeyboardEvent} e The event to process. */ hterm.Keyboard.prototype.onKeyDown_ = function(e) { - if (e.keyCode == 18) + if (e.keyCode == 18) { this.altKeyPressed = this.altKeyPressed | (1 << (e.location - 1)); + } - if (e.keyCode == 27) - this.preventChromeAppNonCtrlShiftDefault_(e); - - var keyDef = this.keyMap.keyDefs[e.keyCode]; + let keyDef = this.keyMap.keyDefs[e.keyCode]; if (!keyDef) { // If this key hasn't been explicitly registered, fall back to the unknown // key mapping (keyCode == 0), and then automatically register it to avoid @@ -6369,40 +7543,45 @@ hterm.Keyboard.prototype.onKeyDown_ = function(e) { } // The type of action we're going to use. - var resolvedActionType = null; + let resolvedActionType = null; - var self = this; - function getAction(name) { + /** + * @param {string} name + * @return {!hterm.Keyboard.KeyDefAction} + */ + const getAction = (name) => { // Get the key action for the given action name. If the action is a // function, dispatch it. If the action defers to the normal action, // resolve that instead. resolvedActionType = name; - var action = keyDef[name]; - if (typeof action == 'function') - action = action.apply(self.keyMap, [e, keyDef]); + let action = keyDef[name]; + if (typeof action == 'function') { + action = action.call(this.keyMap, e, keyDef); + } - if (action === DEFAULT && name != 'normal') + if (action === DEFAULT && name != 'normal') { action = getAction('normal'); + } return action; - } + }; // Note that we use the triple-equals ('===') operator to test equality for // these constants, in order to distinguish usage of the constant from usage // of a literal string that happens to contain the same bytes. - var CANCEL = hterm.Keyboard.KeyActions.CANCEL; - var DEFAULT = hterm.Keyboard.KeyActions.DEFAULT; - var PASS = hterm.Keyboard.KeyActions.PASS; - var STRIP = hterm.Keyboard.KeyActions.STRIP; + const CANCEL = hterm.Keyboard.KeyActions.CANCEL; + const DEFAULT = hterm.Keyboard.KeyActions.DEFAULT; + const PASS = hterm.Keyboard.KeyActions.PASS; + const STRIP = hterm.Keyboard.KeyActions.STRIP; - var control = e.ctrlKey; - var alt = this.altIsMeta ? false : e.altKey; - var meta = this.altIsMeta ? (e.altKey || e.metaKey) : e.metaKey; + let control = e.ctrlKey; + let alt = this.altIsMeta ? false : e.altKey; + let meta = this.altIsMeta ? (e.altKey || e.metaKey) : e.metaKey; // In the key-map, we surround the keyCap for non-printables in "[...]" - var isPrintable = !(/^\[\w+\]$/.test(keyDef.keyCap)); + const isPrintable = !(/^\[\w+\]$/.test(keyDef.keyCap)); switch (this.altGrMode) { case 'ctrl-alt': @@ -6429,7 +7608,8 @@ hterm.Keyboard.prototype.onKeyDown_ = function(e) { break; } - var action; + /** @type {?hterm.Keyboard.KeyDefAction} */ + let action; if (control) { action = getAction('control'); @@ -6443,29 +7623,39 @@ hterm.Keyboard.prototype.onKeyDown_ = function(e) { // If e.maskShiftKey was set (during getAction) it means the shift key is // already accounted for in the action, and we should not act on it any - // further. This is currently only used for Ctrl-Shift-Tab, which should send + // further. This is currently only used for Ctrl+Shift+Tab, which should send // "CSI Z", not "CSI 1 ; 2 Z". - var shift = !e.maskShiftKey && e.shiftKey; + let shift = !e.maskShiftKey && e.shiftKey; - var keyDown = { + /** @type {!hterm.Keyboard.KeyDown} */ + const keyDown = { keyCode: e.keyCode, shift: e.shiftKey, // not `var shift` from above. ctrl: control, alt: alt, - meta: meta + meta: meta, }; - var binding = this.bindings.getBinding(keyDown); + const binding = this.bindings.getBinding(keyDown); if (binding) { // Clear out the modifier bits so we don't try to munge the sequence // further. shift = control = alt = meta = false; resolvedActionType = 'normal'; - action = binding.action; - if (typeof action == 'function') - action = action.call(this, this.terminal, keyDown); + if (typeof binding.action == 'function') { + const bindingFn = + /** @type {!hterm.Keyboard.KeyBindingFunction} */ (binding.action); + action = bindingFn.call(this, this.terminal, keyDown); + } else { + action = /** @type {!hterm.Keyboard.KeyAction} */ (binding.action); + } + } + + // Call keyDef function now that we have given bindings a chance to override. + if (typeof action == 'function') { + action = action.call(this.keyMap, e, keyDef); } if (alt && this.altSendsWhat == 'browser-key' && action == DEFAULT) { @@ -6476,6 +7666,16 @@ hterm.Keyboard.prototype.onKeyDown_ = function(e) { action = PASS; } + // If we are going to handle the key, we most likely want to hide the context + // menu before doing so. This way we hide it when pressing a printable key, + // or navigate (arrow keys/etc...), or press Escape. But we don't want to + // hide it when only pressing modifiers like Alt/Ctrl/Meta because those might + // be used by the OS & hterm to show the context menu in the first place. The + // bare modifier keys are all marked as PASS. + if (action !== PASS) { + this.terminal.contextMenu.hide(); + } + if (action === PASS || (action === DEFAULT && !(control || alt || meta))) { // If this key is supposed to be handled by the browser, or it is an // unmodified key with the default action, then exit this event handler. @@ -6492,18 +7692,21 @@ hterm.Keyboard.prototype.onKeyDown_ = function(e) { if (action === STRIP) { alt = control = false; action = keyDef.normal; - if (typeof action == 'function') - action = action.apply(this.keyMap, [e, keyDef]); + if (typeof action == 'function') { + action = action.call(this.keyMap, e, keyDef); + } - if (action == DEFAULT && keyDef.keyCap.length == 2) + if (action == DEFAULT && keyDef.keyCap.length == 2) { action = keyDef.keyCap.substr((shift ? 1 : 0), 1); + } } e.preventDefault(); e.stopPropagation(); - if (action === CANCEL) + if (action === CANCEL) { return; + } if (action !== DEFAULT && typeof action != 'string') { console.warn('Invalid action: ' + JSON.stringify(action)); @@ -6528,15 +7731,19 @@ hterm.Keyboard.prototype.onKeyDown_ = function(e) { // The math is funky but aligns w/xterm. let imod = 1; - if (shift) + if (shift) { imod += 1; - if (alt) + } + if (alt) { imod += 2; - if (control) + } + if (control) { imod += 4; - if (meta) + } + if (meta) { imod += 8; - let mod = ';' + imod; + } + const mod = ';' + imod; if (action.length == 3) { // Some of the CSI sequences have zero parameters unless modified. @@ -6552,8 +7759,8 @@ hterm.Keyboard.prototype.onKeyDown_ = function(e) { action = keyDef.keyCap.substr((shift ? 1 : 0), 1); if (control) { - var unshifted = keyDef.keyCap.substr(0, 1); - var code = unshifted.charCodeAt(0); + const unshifted = keyDef.keyCap.substr(0, 1); + const code = unshifted.charCodeAt(0); if (code >= 64 && code <= 95) { action = String.fromCharCode(code - 64); } @@ -6561,7 +7768,7 @@ hterm.Keyboard.prototype.onKeyDown_ = function(e) { } if (alt && this.altSendsWhat == '8-bit' && action.length == 1) { - var code = action.charCodeAt(0) + 128; + const code = action.charCodeAt(0) + 128; action = String.fromCharCode(code); } @@ -6574,28 +7781,87 @@ hterm.Keyboard.prototype.onKeyDown_ = function(e) { } } - this.terminal.onVTKeystroke(action); + this.terminal.onVTKeystroke(/** @type {string} */ (action)); }; // SOURCE FILE: hterm/js/hterm_keyboard_bindings.js // Copyright (c) 2015 The Chromium OS Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. +/** + * @typedef {{ + * keyCode: number, + * shift: (boolean|undefined), + * ctrl: (boolean|undefined), + * alt: (boolean|undefined), + * meta: (boolean|undefined), + * }} + */ +hterm.Keyboard.KeyDown; + +/** + * @typedef {function(!hterm.Terminal, !hterm.Keyboard.KeyDown): + * !hterm.Keyboard.KeyAction} + */ +hterm.Keyboard.KeyBindingFunction; + +/** @typedef {!hterm.Keyboard.KeyAction|!hterm.Keyboard.KeyBindingFunction} */ +hterm.Keyboard.KeyBindingAction; + +/** + * @typedef {{ + * keyPattern: !hterm.Keyboard.KeyPattern, + * action: !hterm.Keyboard.KeyBindingAction, + * }} + */ +hterm.Keyboard.KeyBinding; + /** * A mapping from hterm.Keyboard.KeyPattern to an action. * * TODO(rginda): For now this bindings code is only used for user overrides. * hterm.Keyboard.KeyMap still handles all of the built-in key mappings. * It'd be nice if we migrated that over to be hterm.Keyboard.Bindings based. + * + * @constructor */ hterm.Keyboard.Bindings = function() { + /** @private {!Object>} */ this.bindings_ = {}; }; +/** + * Default bindings for each OS. + * + * @type {!Object>} + */ +hterm.Keyboard.Bindings.OsDefaults = { + 'android': { + }, + 'cros': { + // Submit feedback. + 'Alt+Shift+I': 'PASS', + // Toggle chromevox. + 'Ctrl+Alt+Z': 'PASS', + // Switch input method. + 'Ctrl+Space': 'PASS', + }, + 'linux': { + }, + 'mac': { + // Home. + 'Meta+Left': '"\u001b[H"', + // End. + 'Meta+Right': '"\u001b[F"', + }, + 'windows': { + }, +}; + /** * Remove all bindings. */ -hterm.Keyboard.Bindings.prototype.clear = function () { +hterm.Keyboard.Bindings.prototype.clear = function() { this.bindings_ = {}; }; @@ -6605,14 +7871,14 @@ hterm.Keyboard.Bindings.prototype.clear = function () { * Internal API that assumes parsed objects as inputs. * See the public addBinding for more details. * - * @param {hterm.Keyboard.KeyPattern} keyPattern - * @param {string|function|hterm.Keyboard.KeyAction} action + * @param {!hterm.Keyboard.KeyPattern} keyPattern + * @param {!hterm.Keyboard.KeyBindingAction} action */ hterm.Keyboard.Bindings.prototype.addBinding_ = function(keyPattern, action) { - var binding = null; - var list = this.bindings_[keyPattern.keyCode]; + let binding = null; + const list = this.bindings_[keyPattern.keyCode]; if (list) { - for (var i = 0; i < list.length; i++) { + for (let i = 0; i < list.length; i++) { if (list[i].keyPattern.matchKeyPattern(keyPattern)) { binding = list[i]; break; @@ -6644,24 +7910,24 @@ hterm.Keyboard.Bindings.prototype.addBinding_ = function(keyPattern, action) { * If a binding for the keyPattern already exists it will be overridden. * * More specific keyPatterns take precedence over those with wildcards. Given - * bindings for "Ctrl-A" and "Ctrl-*-A", and a "Ctrl-A" keydown, the "Ctrl-A" - * binding will match even if "Ctrl-*-A" was created last. + * bindings for "Ctrl+A" and "Ctrl+*+A", and a "Ctrl+A" keydown, the "Ctrl+A" + * binding will match even if "Ctrl+*+A" was created last. * * If action is a string, it will be passed through hterm.Parser.parseKeyAction. * * For example: - * // Will replace Ctrl-P keystrokes with the string "hiya!". - * addBinding('Ctrl-P', "'hiya!'"); + * // Will replace Ctrl+P keystrokes with the string "hiya!". + * addBinding('Ctrl+P', "'hiya!'"); * // Will cancel the keystroke entirely (make it do nothing). - * addBinding('Alt-D', hterm.Keyboard.KeyActions.CANCEL); + * addBinding('Alt+D', hterm.Keyboard.KeyActions.CANCEL); * // Will execute the code and return the action. - * addBinding('Ctrl-T', function() { + * addBinding('Ctrl+T', function() { * console.log('Got a T!'); * return hterm.Keyboard.KeyActions.PASS; * }); * - * @param {string|hterm.Keyboard.KeyPattern} keyPattern - * @param {string|function|hterm.Keyboard.KeyAction} action + * @param {string|!hterm.Keyboard.KeyPattern} key + * @param {!hterm.Keyboard.KeyBindingAction} action */ hterm.Keyboard.Bindings.prototype.addBinding = function(key, action) { // If we're given a hterm.Keyboard.KeyPattern object, pass it down. @@ -6671,10 +7937,10 @@ hterm.Keyboard.Bindings.prototype.addBinding = function(key, action) { } // Here we treat key as a string. - var p = new hterm.Parser(); + const p = new hterm.Parser(); p.reset(key); - var sequence; + let sequence; try { sequence = p.parseKeySequence(); @@ -6711,20 +7977,26 @@ hterm.Keyboard.Bindings.prototype.addBinding = function(key, action) { * Add multiple bindings at a time using a map of {string: string, ...} * * This uses hterm.Parser to parse the maps key into KeyPatterns, and the - * map values into {string|function|KeyAction}. + * map values into {!hterm.Keyboard.KeyBindingAction}. * * For example: * { - * // Will replace Ctrl-P keystrokes with the string "hiya!". - * 'Ctrl-P': "'hiya!'", + * // Will replace Ctrl+P keystrokes with the string "hiya!". + * 'Ctrl+P': "'hiya!'", * // Will cancel the keystroke entirely (make it do nothing). - * 'Alt-D': hterm.Keyboard.KeyActions.CANCEL, + * 'Alt+D': hterm.Keyboard.KeyActions.CANCEL, * } * - * @param {Object} map + * @param {!Object} map + * @param {boolean=} addOsDefaults If true, OS defaults are added first to + * ensure user defined mappings take precedence. */ -hterm.Keyboard.Bindings.prototype.addBindings = function(map) { - for (var key in map) { +hterm.Keyboard.Bindings.prototype.addBindings = function( + map, addOsDefaults = false) { + if (addOsDefaults) { + this.addBindings(hterm.Keyboard.Bindings.OsDefaults[hterm.os] || {}); + } + for (const key in map) { this.addBinding(key, map[key]); } }; @@ -6733,19 +8005,23 @@ hterm.Keyboard.Bindings.prototype.addBindings = function(map) { * Return the binding that is the best match for the given keyDown record, * or null if there is no match. * - * @param {Object} keyDown An object with a keyCode property and zero or - * more boolean properties representing key modifiers. These property names - * must match those defined in hterm.Keyboard.KeyPattern.modifiers. + * @param {!hterm.Keyboard.KeyDown} keyDown An object with a keyCode property + * and zero or more boolean properties representing key modifiers. These + * property names must match those defined in + * hterm.Keyboard.KeyPattern.modifiers. + * @return {?hterm.Keyboard.KeyBinding} The keyboard binding for this key. */ hterm.Keyboard.Bindings.prototype.getBinding = function(keyDown) { - var list = this.bindings_[keyDown.keyCode]; - if (!list) + const list = this.bindings_[keyDown.keyCode]; + if (!list) { return null; + } - for (var i = 0; i < list.length; i++) { - var binding = list[i]; - if (binding.keyPattern.matchKeyDown(keyDown)) + for (let i = 0; i < list.length; i++) { + const binding = list[i]; + if (binding.keyPattern.matchKeyDown(keyDown)) { return binding; + } } return null; @@ -6755,6 +8031,38 @@ hterm.Keyboard.Bindings.prototype.getBinding = function(keyDown) { // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. +/** + * @typedef {{ + * keyCap: string, + * normal: !hterm.Keyboard.KeyDefAction, + * control: !hterm.Keyboard.KeyDefAction, + * alt: !hterm.Keyboard.KeyDefAction, + * meta: !hterm.Keyboard.KeyDefAction, + * }} + */ +hterm.Keyboard.KeyDef; + +/** + * @typedef {function(!KeyboardEvent, !hterm.Keyboard.KeyDef): + * !hterm.Keyboard.KeyAction} + */ +hterm.Keyboard.KeyDefFunction; + +/** + * @typedef {function(!KeyboardEvent, !hterm.Keyboard.KeyDef): + * !hterm.Keyboard.KeyDefFunction|!hterm.Keyboard.KeyAction} + */ +hterm.Keyboard.KeyDefFunctionProvider; + +/** + * @typedef {( + * !hterm.Keyboard.KeyAction| + * !hterm.Keyboard.KeyDefFunction| + * !hterm.Keyboard.KeyDefFunctionProvider + * )} + */ +hterm.Keyboard.KeyDefAction; + /** * The default key map for hterm. * @@ -6775,9 +8083,13 @@ hterm.Keyboard.Bindings.prototype.getBinding = function(keyDown) { * * The sequences defined in this key map come from [XTERM] as referenced in * vt.js, starting with the section titled "Alt and Meta Keys". + * + * @param {!hterm.Keyboard} keyboard + * @constructor */ hterm.Keyboard.KeyMap = function(keyboard) { this.keyboard = keyboard; + /** @type {!Object} */ this.keyDefs = {}; this.reset(); }; @@ -6814,368 +8126,434 @@ hterm.Keyboard.KeyMap = function(keyboard) { * The second-to-last element of the array will be overwritten with the * state of the modifier keys, as specified in the final table of "PC-Style * Function Keys" from [XTERM]. + * + * @param {number} keyCode The KeyboardEvent.keyCode to match against. + * @param {!hterm.Keyboard.KeyDef} def The actions this key triggers. */ hterm.Keyboard.KeyMap.prototype.addKeyDef = function(keyCode, def) { - if (keyCode in this.keyDefs) + if (keyCode in this.keyDefs) { console.warn('Duplicate keyCode: ' + keyCode); + } this.keyDefs[keyCode] = def; }; -/** - * Add multiple key definitions in a single call. - * - * This function takes the key definitions as variable argument list. Each - * argument is the key definition specified as an array. - * - * (If the function took everything as one big hash we couldn't detect - * duplicates, and there would be a lot more typing involved.) - * - * Each key definition should have 6 elements: (keyCode, keyCap, normal action, - * control action, alt action and meta action). See KeyMap.addKeyDef for the - * meaning of these elements. - */ -hterm.Keyboard.KeyMap.prototype.addKeyDefs = function(var_args) { - for (var i = 0; i < arguments.length; i++) { - this.addKeyDef(arguments[i][0], - { keyCap: arguments[i][1], - normal: arguments[i][2], - control: arguments[i][3], - alt: arguments[i][4], - meta: arguments[i][5] - }); - } -}; - /** * Set up the default state for this keymap. */ hterm.Keyboard.KeyMap.prototype.reset = function() { this.keyDefs = {}; - var self = this; - - // This function is used by the "macro" functions below. It makes it - // possible to use the call() macro as an argument to any other macro. - function resolve(action, e, k) { - if (typeof action == 'function') - return action.apply(self, [e, k]); + const CANCEL = hterm.Keyboard.KeyActions.CANCEL; + const DEFAULT = hterm.Keyboard.KeyActions.DEFAULT; + const PASS = hterm.Keyboard.KeyActions.PASS; + const STRIP = hterm.Keyboard.KeyActions.STRIP; + /** + * This function is used by the "macro" functions below. It makes it + * possible to use the call() macro as an argument to any other macro. + * + * @param {!hterm.Keyboard.KeyDefAction} action + * @param {!KeyboardEvent} e + * @param {!hterm.Keyboard.KeyDef} k + * @return {!hterm.Keyboard.KeyAction} + */ + const resolve = (action, e, k) => { + if (typeof action == 'function') { + const keyDefFn = /** @type {!hterm.Keyboard.KeyDefFunction} */ (action); + return keyDefFn.call(this, e, k); + } return action; - } + }; - // If not application keypad a, else b. The keys that care about - // application keypad ignore it when the key is modified. - function ak(a, b) { - return function(e, k) { - var action = (e.shiftKey || e.ctrlKey || e.altKey || e.metaKey || - !self.keyboard.applicationKeypad) ? a : b; + /** + * If not application keypad a, else b. The keys that care about + * application keypad ignore it when the key is modified. + * + * @param {!hterm.Keyboard.KeyDefAction} a + * @param {!hterm.Keyboard.KeyDefAction} b + * @return {!hterm.Keyboard.KeyDefFunction} + */ + /* TODO(crbug.com/1065216): Delete this if no longer needed. + const ak = (a, b) => { + return (e, k) => { + const action = (e.shiftKey || e.ctrlKey || e.altKey || e.metaKey || + !this.keyboard.applicationKeypad) ? a : b; return resolve(action, e, k); }; - } + }; + */ - // If mod or not application cursor a, else b. The keys that care about - // application cursor ignore it when the key is modified. - function ac(a, b) { - return function(e, k) { - var action = (e.shiftKey || e.ctrlKey || e.altKey || e.metaKey || - !self.keyboard.applicationCursor) ? a : b; + /** + * If mod or not application cursor a, else b. The keys that care about + * application cursor ignore it when the key is modified. + * + * @param {!hterm.Keyboard.KeyDefAction} a + * @param {!hterm.Keyboard.KeyDefAction} b + * @return {!hterm.Keyboard.KeyDefFunction} + */ + const ac = (a, b) => { + return (e, k) => { + const action = (e.shiftKey || e.ctrlKey || e.altKey || e.metaKey || + !this.keyboard.applicationCursor) ? a : b; return resolve(action, e, k); }; - } + }; - // If not backspace-sends-backspace keypad a, else b. - function bs(a, b) { - return function(e, k) { - var action = !self.keyboard.backspaceSendsBackspace ? a : b; + /** + * If not backspace-sends-backspace keypad a, else b. + * + * @param {!hterm.Keyboard.KeyDefAction} a + * @param {!hterm.Keyboard.KeyDefAction} b + * @return {!hterm.Keyboard.KeyDefFunction} + */ + const bs = (a, b) => { + return (e, k) => { + const action = !this.keyboard.backspaceSendsBackspace ? a : b; return resolve(action, e, k); }; - } + }; - // If not e.shiftKey a, else b. - function sh(a, b) { - return function(e, k) { - var action = !e.shiftKey ? a : b; + /** + * If not e.shiftKey a, else b. + * + * @param {!hterm.Keyboard.KeyDefAction} a + * @param {!hterm.Keyboard.KeyDefAction} b + * @return {!hterm.Keyboard.KeyDefFunction} + */ + const sh = (a, b) => { + return (e, k) => { + const action = !e.shiftKey ? a : b; e.maskShiftKey = true; return resolve(action, e, k); }; - } + }; - // If not e.altKey a, else b. - function alt(a, b) { - return function(e, k) { - var action = !e.altKey ? a : b; + /** + * If not e.altKey a, else b. + * + * @param {!hterm.Keyboard.KeyDefAction} a + * @param {!hterm.Keyboard.KeyDefAction} b + * @return {!hterm.Keyboard.KeyDefFunction} + */ + const alt = (a, b) => { + return (e, k) => { + const action = !e.altKey ? a : b; return resolve(action, e, k); }; - } + }; - // If no modifiers a, else b. - function mod(a, b) { - return function(e, k) { - var action = !(e.shiftKey || e.ctrlKey || e.altKey || e.metaKey) ? a : b; + /** + * If no modifiers a, else b. + * + * @param {!hterm.Keyboard.KeyDefAction} a + * @param {!hterm.Keyboard.KeyDefAction} b + * @return {!hterm.Keyboard.KeyDefFunction} + */ + const mod = (a, b) => { + return (e, k) => { + const action = !(e.shiftKey || e.ctrlKey || e.altKey || e.metaKey) ? + a : b; return resolve(action, e, k); }; - } + }; - // Compute a control character for a given character. - function ctl(ch) { return String.fromCharCode(ch.charCodeAt(0) - 64); } + /** + * Compute a control character for a given character. + * + * @param {string} ch + * @return {string} + */ + const ctl = (ch) => String.fromCharCode(ch.charCodeAt(0) - 64); - // Call a method on the keymap instance. - function c(m) { return function(e, k) { return this[m](e, k); }; } + /** + * Call a method on the keymap instance. + * + * @param {string} m name of method to call. + * @return {( + * !hterm.Keyboard.KeyDefFunction| + * !hterm.Keyboard.KeyDefFunctionProvider + * )} + */ + const c = (m) => { + return (e, k) => this[m](e, k); + }; // Ignore if not trapping media keys. - function med(fn) { - return function(e, k) { - if (!self.keyboard.mediaKeysAreFKeys) { + const med = (fn) => { + return (e, k) => { + if (!this.keyboard.mediaKeysAreFKeys) { // Block Back, Forward, and Reload keys to avoid navigating away from // the current page. return (e.keyCode == 166 || e.keyCode == 167 || e.keyCode == 168) ? - hterm.Keyboard.KeyActions.CANCEL : - hterm.Keyboard.KeyActions.PASS; + CANCEL : PASS; } return resolve(fn, e, k); }; - } + }; - // Browser-specific differences. - if (window.navigator && navigator.userAgent) { - if (navigator.userAgent.includes('Firefox')) { - // Firefox defines some keys uniquely. No other browser defines these is - // this way. Some even conflict. The keyCode field isn't well documented - // as it isn't standardized. At some point we should switch to "key". - // https://developer.mozilla.org/en-US/docs/Web/API/KeyboardEvent/keyCode - // http://unixpapa.com/js/key.html - var keycapMute = 181; // Mute - var keycapVolDn = 182; // Volume Down - var keycapVolUp = 183; // Volume Up - var keycapSC = 59; // ;: - var keycapEP = 61; // =+ - var keycapMU = 173; // -_ + /** + * @param {number} keyCode + * @param {string} keyCap + * @param {!hterm.Keyboard.KeyDefAction} normal + * @param {!hterm.Keyboard.KeyDefAction} control + * @param {!hterm.Keyboard.KeyDefAction} alt + * @param {!hterm.Keyboard.KeyDefAction} meta + */ + const add = (keyCode, keyCap, normal, control, alt, meta) => { + this.addKeyDef(keyCode, { + keyCap: keyCap, + normal: normal, + control: control, + alt: alt, + meta: meta, + }); + }; - } else { - // All other browsers use these mappings. - var keycapMute = 173; // Mute - var keycapVolDn = 174; // Volume Down - var keycapVolUp = 175; // Volume Up - var keycapSC = 186; // ;: - var keycapEP = 187; // =+ - var keycapMU = 189; // -_ - } - } - - var ESC = '\x1b'; - var CSI = '\x1b['; - var SS3 = '\x1bO'; - - var CANCEL = hterm.Keyboard.KeyActions.CANCEL; - var DEFAULT = hterm.Keyboard.KeyActions.DEFAULT; - var PASS = hterm.Keyboard.KeyActions.PASS; - var STRIP = hterm.Keyboard.KeyActions.STRIP; - - this.addKeyDefs( - // These fields are: [keycode, keycap, normal, control, alt, meta] - - // The browser sends the keycode 0 for some keys. We'll just assume it's - // going to do the right thing by default for those keys. - [0, '[UNKNOWN]', PASS, PASS, PASS, PASS], - - // First row. - // These bindings match xterm for lack of a better standard. The emitted - // values might look like they're skipping values, but it's what xterm does. - // https://invisible-island.net/xterm/ctlseqs/ctlseqs.html#h2-PC-Style-Function-Keys - [27, '[ESC]', ESC, DEFAULT, DEFAULT, DEFAULT], - [112, '[F1]', mod(SS3 + 'P', CSI + 'P'), DEFAULT, CSI + "23~", DEFAULT], - [113, '[F2]', mod(SS3 + 'Q', CSI + 'Q'), DEFAULT, CSI + "24~", DEFAULT], - [114, '[F3]', mod(SS3 + 'R', CSI + 'R'), DEFAULT, CSI + "25~", DEFAULT], - [115, '[F4]', mod(SS3 + 'S', CSI + 'S'), DEFAULT, CSI + "26~", DEFAULT], - [116, '[F5]', CSI + '15~', DEFAULT, CSI + "28~", DEFAULT], - [117, '[F6]', CSI + '17~', DEFAULT, CSI + "29~", DEFAULT], - [118, '[F7]', CSI + '18~', DEFAULT, CSI + "31~", DEFAULT], - [119, '[F8]', CSI + '19~', DEFAULT, CSI + "32~", DEFAULT], - [120, '[F9]', CSI + '20~', DEFAULT, CSI + "33~", DEFAULT], - [121, '[F10]', CSI + '21~', DEFAULT, CSI + "34~", DEFAULT], - [122, '[F11]', c('onF11_'), DEFAULT, CSI + "42~", DEFAULT], - [123, '[F12]', CSI + '24~', DEFAULT, CSI + "43~", DEFAULT], - - // Second row. - [192, '`~', DEFAULT, sh(ctl('@'), ctl('^')), DEFAULT, PASS], - [49, '1!', DEFAULT, c('onCtrlNum_'), c('onAltNum_'), c('onMetaNum_')], - [50, '2@', DEFAULT, c('onCtrlNum_'), c('onAltNum_'), c('onMetaNum_')], - [51, '3#', DEFAULT, c('onCtrlNum_'), c('onAltNum_'), c('onMetaNum_')], - [52, '4$', DEFAULT, c('onCtrlNum_'), c('onAltNum_'), c('onMetaNum_')], - [53, '5%', DEFAULT, c('onCtrlNum_'), c('onAltNum_'), c('onMetaNum_')], - [54, '6^', DEFAULT, c('onCtrlNum_'), c('onAltNum_'), c('onMetaNum_')], - [55, '7&', DEFAULT, c('onCtrlNum_'), c('onAltNum_'), c('onMetaNum_')], - [56, '8*', DEFAULT, c('onCtrlNum_'), c('onAltNum_'), c('onMetaNum_')], - [57, '9(', DEFAULT, c('onCtrlNum_'), c('onAltNum_'), c('onMetaNum_')], - [48, '0)', DEFAULT, c('onPlusMinusZero_'),c('onAltNum_'),c('onPlusMinusZero_')], - [keycapMU, '-_', DEFAULT, c('onPlusMinusZero_'), DEFAULT, c('onPlusMinusZero_')], - [keycapEP, '=+', DEFAULT, c('onPlusMinusZero_'), DEFAULT, c('onPlusMinusZero_')], - - // was Firefox Italian +* but applies to all browsers (and lots of EU keyboard layouts). - // see https://github.com/OSC/ondemand/issues/1214 - [171, '+*', DEFAULT, c('onPlusMinusZero_'), DEFAULT, c('onPlusMinusZero_')], - - [8, '[BKSP]', bs('\x7f', '\b'), bs('\b', '\x7f'), DEFAULT, DEFAULT], - - // Third row. - [9, '[TAB]', sh('\t', CSI + 'Z'), STRIP, PASS, DEFAULT], - [81, 'qQ', DEFAULT, ctl('Q'), DEFAULT, DEFAULT], - [87, 'wW', DEFAULT, ctl('W'), DEFAULT, DEFAULT], - [69, 'eE', DEFAULT, ctl('E'), DEFAULT, DEFAULT], - [82, 'rR', DEFAULT, ctl('R'), DEFAULT, DEFAULT], - [84, 'tT', DEFAULT, ctl('T'), DEFAULT, DEFAULT], - [89, 'yY', DEFAULT, ctl('Y'), DEFAULT, DEFAULT], - [85, 'uU', DEFAULT, ctl('U'), DEFAULT, DEFAULT], - [73, 'iI', DEFAULT, ctl('I'), DEFAULT, DEFAULT], - [79, 'oO', DEFAULT, ctl('O'), DEFAULT, DEFAULT], - [80, 'pP', DEFAULT, ctl('P'), DEFAULT, DEFAULT], - [219, '[{', DEFAULT, ctl('['), DEFAULT, DEFAULT], - [221, ']}', DEFAULT, ctl(']'), DEFAULT, DEFAULT], - [220, '\\|', DEFAULT, ctl('\\'), DEFAULT, DEFAULT], - - // Fourth row. We let Ctrl-Shift-J pass for Chrome DevTools. - // To be compliant with xterm's behavior for modifiers on Enter - // would mean maximizing the window with Alt-Enter... so we don't - // want to do that. Our behavior on Enter is what most other - // modern emulators do. - [20, '[CAPS]', PASS, PASS, PASS, DEFAULT], - [65, 'aA', DEFAULT, ctl('A'), DEFAULT, DEFAULT], - [83, 'sS', DEFAULT, ctl('S'), DEFAULT, DEFAULT], - [68, 'dD', DEFAULT, ctl('D'), DEFAULT, DEFAULT], - [70, 'fF', DEFAULT, ctl('F'), DEFAULT, DEFAULT], - [71, 'gG', DEFAULT, ctl('G'), DEFAULT, DEFAULT], - [72, 'hH', DEFAULT, ctl('H'), DEFAULT, DEFAULT], - [74, 'jJ', DEFAULT, sh(ctl('J'), PASS), DEFAULT, DEFAULT], - [75, 'kK', DEFAULT, sh(ctl('K'), c('onClear_')), DEFAULT, DEFAULT], - [76, 'lL', DEFAULT, sh(ctl('L'), PASS), DEFAULT, DEFAULT], - [keycapSC, ';:', DEFAULT, STRIP, DEFAULT, DEFAULT], - [222, '\'"', DEFAULT, STRIP, DEFAULT, DEFAULT], - [13, '[ENTER]', '\r', DEFAULT, DEFAULT, DEFAULT], - - // Fifth row. This includes the copy/paste shortcuts. On some - // platforms it's Ctrl-C/V, on others it's Meta-C/V. We assume either - // Ctrl-C/Meta-C should pass to the browser when there is a selection, - // and Ctrl-Shift-V/Meta-*-V should always pass to the browser (since - // these seem to be recognized as paste too). - [16, '[SHIFT]', PASS, PASS, PASS, DEFAULT], - [90, 'zZ', DEFAULT, ctl('Z'), DEFAULT, DEFAULT], - [88, 'xX', DEFAULT, ctl('X'), DEFAULT, DEFAULT], - [67, 'cC', DEFAULT, c('onCtrlC_'), DEFAULT, c('onMetaC_')], - [86, 'vV', DEFAULT, c('onCtrlV_'), DEFAULT, c('onMetaV_')], - [66, 'bB', DEFAULT, sh(ctl('B'), PASS), DEFAULT, sh(DEFAULT, PASS)], - [78, 'nN', DEFAULT, c('onCtrlN_'), DEFAULT, c('onMetaN_')], - [77, 'mM', DEFAULT, ctl('M'), DEFAULT, DEFAULT], - [188, ',<', DEFAULT, alt(STRIP, PASS), DEFAULT, DEFAULT], - [190, '.>', DEFAULT, alt(STRIP, PASS), DEFAULT, DEFAULT], - [191, '/?', DEFAULT, sh(ctl('_'), ctl('?')), DEFAULT, DEFAULT], - - // Sixth and final row. - [17, '[CTRL]', PASS, PASS, PASS, PASS], - [18, '[ALT]', PASS, PASS, PASS, PASS], - [91, '[LAPL]', PASS, PASS, PASS, PASS], - [32, ' ', DEFAULT, ctl('@'), DEFAULT, DEFAULT], - [92, '[RAPL]', PASS, PASS, PASS, PASS], - [93, '[RMENU]', PASS, PASS, PASS, PASS], - - // These things. - [42, '[PRTSCR]', PASS, PASS, PASS, PASS], - [145, '[SCRLK]', PASS, PASS, PASS, PASS], - [19, '[BREAK]', PASS, PASS, PASS, PASS], - - // The block of six keys above the arrows. - [45, '[INSERT]', c('onKeyInsert_'), DEFAULT, DEFAULT, DEFAULT], - [36, '[HOME]', c('onKeyHome_'), DEFAULT, DEFAULT, DEFAULT], - [33, '[PGUP]', c('onKeyPageUp_'), DEFAULT, DEFAULT, DEFAULT], - [46, '[DEL]', c('onKeyDel_'), DEFAULT, DEFAULT, DEFAULT], - [35, '[END]', c('onKeyEnd_'), DEFAULT, DEFAULT, DEFAULT], - [34, '[PGDOWN]', c('onKeyPageDown_'), DEFAULT, DEFAULT, DEFAULT], - - // Arrow keys. When unmodified they respect the application cursor state, - // otherwise they always send the CSI codes. - [38, '[UP]', c('onKeyArrowUp_'), DEFAULT, DEFAULT, DEFAULT], - [40, '[DOWN]', c('onKeyArrowDown_'), DEFAULT, DEFAULT, DEFAULT], - [39, '[RIGHT]', ac(CSI + 'C', SS3 + 'C'), DEFAULT, DEFAULT, DEFAULT], - [37, '[LEFT]', ac(CSI + 'D', SS3 + 'D'), DEFAULT, DEFAULT, DEFAULT], - - [144, '[NUMLOCK]', PASS, PASS, PASS, PASS], - - // On Apple keyboards, the NumLock key is a Clear key. It also tends to be - // what KP5 sends when numlock is off. Not clear if we could do anything - // useful with it, so just pass it along. - [12, '[CLEAR]', PASS, PASS, PASS, PASS], - - // With numlock off, the keypad generates the same key codes as the arrows - // and 'block of six' for some keys, and null key codes for the rest. - - // Keypad with numlock on generates unique key codes... - [96, '[KP0]', DEFAULT, DEFAULT, DEFAULT, DEFAULT], - [97, '[KP1]', DEFAULT, DEFAULT, DEFAULT, DEFAULT], - [98, '[KP2]', DEFAULT, DEFAULT, DEFAULT, DEFAULT], - [99, '[KP3]', DEFAULT, DEFAULT, DEFAULT, DEFAULT], - [100, '[KP4]', DEFAULT, DEFAULT, DEFAULT, DEFAULT], - [101, '[KP5]', DEFAULT, DEFAULT, DEFAULT, DEFAULT], - [102, '[KP6]', DEFAULT, DEFAULT, DEFAULT, DEFAULT], - [103, '[KP7]', DEFAULT, DEFAULT, DEFAULT, DEFAULT], - [104, '[KP8]', DEFAULT, DEFAULT, DEFAULT, DEFAULT], - [105, '[KP9]', DEFAULT, DEFAULT, DEFAULT, DEFAULT], - [107, '[KP+]', DEFAULT, c('onPlusMinusZero_'), DEFAULT, c('onPlusMinusZero_')], - [109, '[KP-]', DEFAULT, c('onPlusMinusZero_'), DEFAULT, c('onPlusMinusZero_')], - [106, '[KP*]', DEFAULT, DEFAULT, DEFAULT, DEFAULT], - [111, '[KP/]', DEFAULT, DEFAULT, DEFAULT, DEFAULT], - [110, '[KP.]', DEFAULT, DEFAULT, DEFAULT, DEFAULT] - ); + // Browser-specific differences. + // let keycapMute; + // let keycapVolDn; + // let keycapVolDn + let keycapSC; + let keycapEP; + let keycapMU; + if (window.navigator && navigator.userAgent && + navigator.userAgent.includes('Firefox')) { + // Firefox defines some keys uniquely. No other browser defines these in + // this way. Some even conflict. The keyCode field isn't well documented + // as it isn't standardized. At some point we should switch to "key". + // https://developer.mozilla.org/en-US/docs/Web/API/KeyboardEvent/keyCode + // http://unixpapa.com/js/key.html + // keycapMute = 181; // Mute + // keycapVolDn = 182; // Volume Down + // keycapVolUp = 183; // Volume Up + keycapSC = 59; // ;: + keycapEP = 61; // =+ + keycapMU = 173; // -_ + } else { + // All other browsers use these mappings. + // keycapMute = 173; // Mute + // keycapVolDn = 174; // Volume Down + // keycapVolUp = 175; // Volume Up + keycapSC = 186; // ;: + keycapEP = 187; // =+ + keycapMU = 189; // -_ + } + + const ESC = '\x1b'; + const CSI = '\x1b['; + const SS3 = '\x1bO'; + + // These fields are: [keycode, keycap, normal, control, alt, meta] + /* eslint-disable no-multi-spaces */ + + // The browser sends the keycode 0 for some keys. We'll just assume it's + // going to do the right thing by default for those keys. + add(0, '[UNKNOWN]', PASS, PASS, PASS, PASS); + + // First row. + // These bindings match xterm for lack of a better standard. The emitted + // values might look like they're skipping values, but it's what xterm does. + // https://invisible-island.net/xterm/ctlseqs/ctlseqs.html#h2-PC-Style-Function-Keys + add(27, '[ESC]', ESC, DEFAULT, DEFAULT, DEFAULT); + add(112, '[F1]', mod(SS3 + 'P', CSI + 'P'), DEFAULT, CSI + '23~', DEFAULT); + add(113, '[F2]', mod(SS3 + 'Q', CSI + 'Q'), DEFAULT, CSI + '24~', DEFAULT); + add(114, '[F3]', mod(SS3 + 'R', CSI + 'R'), DEFAULT, CSI + '25~', DEFAULT); + add(115, '[F4]', mod(SS3 + 'S', CSI + 'S'), DEFAULT, CSI + '26~', DEFAULT); + add(116, '[F5]', CSI + '15~', DEFAULT, CSI + '28~', DEFAULT); + add(117, '[F6]', CSI + '17~', DEFAULT, CSI + '29~', DEFAULT); + add(118, '[F7]', CSI + '18~', DEFAULT, CSI + '31~', DEFAULT); + add(119, '[F8]', CSI + '19~', DEFAULT, CSI + '32~', DEFAULT); + add(120, '[F9]', CSI + '20~', DEFAULT, CSI + '33~', DEFAULT); + add(121, '[F10]', CSI + '21~', DEFAULT, CSI + '34~', DEFAULT); + add(122, '[F11]', c('onF11_'), DEFAULT, CSI + '42~', DEFAULT); + add(123, '[F12]', CSI + '24~', DEFAULT, CSI + '43~', DEFAULT); + + // Second row. + add(192, '`~', DEFAULT, sh(ctl('@'), ctl('^')), DEFAULT, PASS); + add(49, '1!', DEFAULT, c('onCtrlNum_'), c('onAltNum_'), c('onMetaNum_')); + add(50, '2@', DEFAULT, c('onCtrlNum_'), c('onAltNum_'), c('onMetaNum_')); + add(51, '3#', DEFAULT, c('onCtrlNum_'), c('onAltNum_'), c('onMetaNum_')); + add(52, '4$', DEFAULT, c('onCtrlNum_'), c('onAltNum_'), c('onMetaNum_')); + add(53, '5%', DEFAULT, c('onCtrlNum_'), c('onAltNum_'), c('onMetaNum_')); + add(54, '6^', DEFAULT, c('onCtrlNum_'), c('onAltNum_'), c('onMetaNum_')); + add(55, '7&', DEFAULT, c('onCtrlNum_'), c('onAltNum_'), c('onMetaNum_')); + add(56, '8*', DEFAULT, c('onCtrlNum_'), c('onAltNum_'), c('onMetaNum_')); + add(57, '9(', DEFAULT, c('onCtrlNum_'), c('onAltNum_'), c('onMetaNum_')); + add(48, '0)', DEFAULT, c('onZoom_'), c('onAltNum_'), c('onZoom_')); + add(keycapMU, '-_', DEFAULT, c('onZoom_'), DEFAULT, c('onZoom_')); + add(keycapEP, '=+', DEFAULT, c('onZoom_'), DEFAULT, c('onZoom_')); + + // Firefox Italian +*. + add(171, '+*', DEFAULT, c('onZoom_'), DEFAULT, c('onZoom_')); + + add(8, '[BKSP]', bs('\x7f', '\b'), bs('\b', '\x7f'), DEFAULT, DEFAULT); + + // Third row. + add(9, '[TAB]', sh('\t', CSI + 'Z'), c('onCtrlTab_'), PASS, DEFAULT); + add(81, 'qQ', DEFAULT, ctl('Q'), DEFAULT, DEFAULT); + add(87, 'wW', DEFAULT, c('onCtrlW_'), DEFAULT, DEFAULT); + add(69, 'eE', DEFAULT, ctl('E'), DEFAULT, DEFAULT); + add(82, 'rR', DEFAULT, ctl('R'), DEFAULT, DEFAULT); + add(84, 'tT', DEFAULT, c('onCtrlT_'), DEFAULT, DEFAULT); + add(89, 'yY', DEFAULT, ctl('Y'), DEFAULT, DEFAULT); + add(85, 'uU', DEFAULT, ctl('U'), DEFAULT, DEFAULT); + add(73, 'iI', DEFAULT, ctl('I'), DEFAULT, DEFAULT); + add(79, 'oO', DEFAULT, ctl('O'), DEFAULT, DEFAULT); + add(80, 'pP', DEFAULT, ctl('P'), DEFAULT, DEFAULT); + add(219, '[{', DEFAULT, ctl('['), DEFAULT, DEFAULT); + add(221, ']}', DEFAULT, ctl(']'), DEFAULT, DEFAULT); + add(220, '\\|', DEFAULT, ctl('\\'), DEFAULT, DEFAULT); + + // Fourth row. We let Ctrl+Shift+J pass for Chrome DevTools. + // To be compliant with xterm's behavior for modifiers on Enter + // would mean maximizing the window with Alt+Enter... so we don't + // want to do that. Our behavior on Enter is what most other + // modern emulators do. + add(20, '[CAPS]', PASS, PASS, PASS, DEFAULT); + add(65, 'aA', DEFAULT, ctl('A'), DEFAULT, DEFAULT); + add(83, 'sS', DEFAULT, ctl('S'), DEFAULT, DEFAULT); + add(68, 'dD', DEFAULT, ctl('D'), DEFAULT, DEFAULT); + add(70, 'fF', DEFAULT, sh(ctl('F'), c('onCtrlShiftF_')), DEFAULT, DEFAULT); + add(71, 'gG', DEFAULT, ctl('G'), DEFAULT, DEFAULT); + add(72, 'hH', DEFAULT, ctl('H'), DEFAULT, DEFAULT); + add(74, 'jJ', DEFAULT, sh(ctl('J'), PASS), DEFAULT, DEFAULT); + add(75, 'kK', DEFAULT, sh(ctl('K'), c('onClear_')), DEFAULT, DEFAULT); + add(76, 'lL', DEFAULT, sh(ctl('L'), PASS), DEFAULT, DEFAULT); + add(keycapSC, ';:', DEFAULT, STRIP, DEFAULT, DEFAULT); + add(222, '\'"', DEFAULT, STRIP, DEFAULT, DEFAULT); + add(13, '[ENTER]', '\r', DEFAULT, DEFAULT, DEFAULT); + + // Fifth row. This includes the copy/paste shortcuts. On some + // platforms it's Ctrl+C/V, on others it's Meta+C/V. We assume either + // Ctrl+C/Meta+C should pass to the browser when there is a selection, + // and Ctrl+Shift+V/Meta+*+V should always pass to the browser (since + // these seem to be recognized as paste too). + add(16, '[SHIFT]', PASS, PASS, PASS, DEFAULT); + add(90, 'zZ', DEFAULT, ctl('Z'), DEFAULT, DEFAULT); + add(88, 'xX', DEFAULT, ctl('X'), DEFAULT, DEFAULT); + add(67, 'cC', DEFAULT, c('onCtrlC_'), DEFAULT, c('onMetaC_')); + add(86, 'vV', DEFAULT, c('onCtrlV_'), DEFAULT, c('onMetaV_')); + add(66, 'bB', DEFAULT, ctl('B'), DEFAULT, DEFAULT); + add(78, 'nN', DEFAULT, c('onCtrlN_'), DEFAULT, c('onMetaN_')); + add(77, 'mM', DEFAULT, ctl('M'), DEFAULT, DEFAULT); + add(188, ',<', DEFAULT, alt(STRIP, PASS), DEFAULT, DEFAULT); + add(190, '.>', DEFAULT, alt(STRIP, PASS), DEFAULT, DEFAULT); + add(191, '/?', DEFAULT, sh(ctl('_'), ctl('?')), DEFAULT, DEFAULT); + + // Sixth and final row. + add(17, '[CTRL]', PASS, PASS, PASS, PASS); + add(18, '[ALT]', PASS, PASS, PASS, PASS); + add(91, '[LAPL]', PASS, PASS, PASS, PASS); + add(32, ' ', DEFAULT, ctl('@'), DEFAULT, DEFAULT); + add(92, '[RAPL]', PASS, PASS, PASS, PASS); + add(93, '[RMENU]', PASS, PASS, PASS, PASS); + + // These things. + add(42, '[PRTSCR]', PASS, PASS, PASS, PASS); + add(145, '[SCRLK]', PASS, PASS, PASS, PASS); + add(19, '[BREAK]', PASS, PASS, PASS, PASS); + + // The block of six keys above the arrows. + add(45, '[INSERT]', c('onKeyInsert_'), DEFAULT, DEFAULT, DEFAULT); + add(36, '[HOME]', c('onKeyHome_'), DEFAULT, DEFAULT, DEFAULT); + add(33, '[PGUP]', c('onKeyPageUp_'), DEFAULT, DEFAULT, DEFAULT); + add(46, '[DEL]', c('onKeyDel_'), DEFAULT, DEFAULT, DEFAULT); + add(35, '[END]', c('onKeyEnd_'), DEFAULT, DEFAULT, DEFAULT); + add(34, '[PGDOWN]', c('onKeyPageDown_'), DEFAULT, DEFAULT, DEFAULT); + + // Arrow keys. When unmodified they respect the application cursor state, + // otherwise they always send the CSI codes. + add(38, '[UP]', c('onKeyArrowUp_'), DEFAULT, DEFAULT, DEFAULT); + add(40, '[DOWN]', c('onKeyArrowDown_'), DEFAULT, DEFAULT, DEFAULT); + add(39, '[RIGHT]', ac(CSI + 'C', SS3 + 'C'), DEFAULT, DEFAULT, DEFAULT); + add(37, '[LEFT]', ac(CSI + 'D', SS3 + 'D'), DEFAULT, DEFAULT, DEFAULT); + + add(144, '[NUMLOCK]', PASS, PASS, PASS, PASS); + + // On Apple keyboards, the NumLock key is a Clear key. It also tends to be + // what KP5 sends when numlock is off. Not clear if we could do anything + // useful with it, so just pass it along. + add(12, '[CLEAR]', PASS, PASS, PASS, PASS); + + // With numlock off, the keypad generates the same key codes as the arrows + // and 'block of six' for some keys, and null key codes for the rest. + + // Keypad with numlock on generates unique key codes... + add(96, '[KP0]', DEFAULT, DEFAULT, DEFAULT, DEFAULT); + add(97, '[KP1]', DEFAULT, DEFAULT, DEFAULT, DEFAULT); + add(98, '[KP2]', DEFAULT, DEFAULT, DEFAULT, DEFAULT); + add(99, '[KP3]', DEFAULT, DEFAULT, DEFAULT, DEFAULT); + add(100, '[KP4]', DEFAULT, DEFAULT, DEFAULT, DEFAULT); + add(101, '[KP5]', DEFAULT, DEFAULT, DEFAULT, DEFAULT); + add(102, '[KP6]', DEFAULT, DEFAULT, DEFAULT, DEFAULT); + add(103, '[KP7]', DEFAULT, DEFAULT, DEFAULT, DEFAULT); + add(104, '[KP8]', DEFAULT, DEFAULT, DEFAULT, DEFAULT); + add(105, '[KP9]', DEFAULT, DEFAULT, DEFAULT, DEFAULT); + add(107, '[KP+]', DEFAULT, c('onZoom_'), DEFAULT, c('onZoom_')); + add(109, '[KP-]', DEFAULT, c('onZoom_'), DEFAULT, c('onZoom_')); + add(106, '[KP*]', DEFAULT, DEFAULT, DEFAULT, DEFAULT); + add(111, '[KP/]', DEFAULT, DEFAULT, DEFAULT, DEFAULT); + add(110, '[KP.]', DEFAULT, DEFAULT, DEFAULT, DEFAULT); // OS-specific differences. if (hterm.os == 'cros') { - this.addKeyDefs( - // Chrome OS keyboard top row. The media-keys-are-fkeys preference allows - // users to make these always behave as function keys (see those bindings - // above for more details). - [166, '[BACK]', med(mod(SS3+'P', CSI+'P')), DEFAULT, CSI+'23~', DEFAULT], // F1 - [167, '[FWD]', med(mod(SS3+'Q', CSI+'Q')), DEFAULT, CSI+'24~', DEFAULT], // F2 - [168, '[RELOAD]', med(mod(SS3+'R', CSI+'R')), DEFAULT, CSI+'25~', DEFAULT], // F3 - [183, '[FSCR]', med(mod(SS3+'S', CSI+'S')), DEFAULT, CSI+'26~', DEFAULT], // F4 - [182, '[WINS]', med(CSI + '15~'), DEFAULT, CSI+'28~', DEFAULT], // F5 - [216, '[BRIT-]', med(CSI + '17~'), DEFAULT, CSI+'29~', DEFAULT], // F6 - [217, '[BRIT+]', med(CSI + '18~'), DEFAULT, CSI+'31~', DEFAULT], // F7 - [173, '[MUTE]', med(CSI + '19~'), DEFAULT, CSI+'32~', DEFAULT], // F8 - [174, '[VOL-]', med(CSI + '20~'), DEFAULT, CSI+'33~', DEFAULT], // F9 - [175, '[VOL+]', med(CSI + '21~'), DEFAULT, CSI+'34~', DEFAULT], // F10 - - // We could make this into F11, but it'd be a bit weird. Chrome allows us - // to see this and react, but it doesn't actually allow us to block or - // cancel it, so it makes the screen flash/lock still. - [152, '[POWER]', DEFAULT, DEFAULT, DEFAULT, DEFAULT], - - // The Pixelbook has a slightly different layout. This means half the keys - // above are off by one. https://crbug.com/807513 - [179, '[PLAY]', med(CSI + '18~'), DEFAULT, CSI + '31~', DEFAULT], // F7 - // The settings / hamburgers / three hot dogs / menu / whatever-it's-called. - [154, '[DOGS]', med(CSI + '23~'), DEFAULT, CSI + '42~', DEFAULT], // F11 - - // We don't use this for anything, but keep it from popping up by default. - [153, '[ASSIST]', DEFAULT, DEFAULT, DEFAULT, DEFAULT] - ); - } + // Chrome OS keyboard top row. The media-keys-are-fkeys preference allows + // users to make these always behave as function keys (see those bindings + // above for more details). + /* eslint-disable max-len */ + add(166, '[BACK]', med(mod(SS3 + 'P', CSI + 'P')), DEFAULT, CSI + '23~', DEFAULT); // F1 + add(167, '[FWD]', med(mod(SS3 + 'Q', CSI + 'Q')), DEFAULT, CSI + '24~', DEFAULT); // F2 + add(168, '[RELOAD]', med(mod(SS3 + 'R', CSI + 'R')), DEFAULT, CSI + '25~', DEFAULT); // F3 + add(183, '[FSCR]', med(mod(SS3 + 'S', CSI + 'S')), DEFAULT, CSI + '26~', DEFAULT); // F4 + add(182, '[WINS]', med(CSI + '15~'), DEFAULT, CSI + '28~', DEFAULT); // F5 + add(216, '[BRIT-]', med(CSI + '17~'), DEFAULT, CSI + '29~', DEFAULT); // F6 + add(217, '[BRIT+]', med(CSI + '18~'), DEFAULT, CSI + '31~', DEFAULT); // F7 + add(173, '[MUTE]', med(CSI + '19~'), DEFAULT, CSI + '32~', DEFAULT); // F8 + add(174, '[VOL-]', med(CSI + '20~'), DEFAULT, CSI + '33~', DEFAULT); // F9 + add(175, '[VOL+]', med(CSI + '21~'), DEFAULT, CSI + '34~', DEFAULT); // F10 + /* eslint-enable max-len */ + + // We could make this into F11, but it'd be a bit weird. Chrome allows us + // to see this and react, but it doesn't actually allow us to block or + // cancel it, so it makes the screen flash/lock still. + add(152, '[POWER]', DEFAULT, DEFAULT, DEFAULT, DEFAULT); + + // The Pixelbook has a slightly different layout. This means half the keys + // above are off by one. https://crbug.com/807513 + add(179, '[PLAY]', med(CSI + '18~'), DEFAULT, CSI + '31~', DEFAULT); // F7 + // The settings / hamburgers / three hot dogs / menu / whatever-it's-called. + add(154, '[DOGS]', med(CSI + '23~'), DEFAULT, CSI + '42~', DEFAULT); // F11 + + // We don't use this for anything, but keep it from popping up by default. + add(153, '[ASSIST]', DEFAULT, DEFAULT, DEFAULT, DEFAULT); + } + /* eslint-enable no-multi-spaces */ }; /** * Either allow the paste or send a key sequence. + * + * @param {!KeyboardEvent} e The event to process. + * @return {symbol|string} Key action or sequence. */ hterm.Keyboard.KeyMap.prototype.onKeyInsert_ = function(e) { - if (this.keyboard.shiftInsertPaste && e.shiftKey) + if (this.keyboard.shiftInsertPaste && e.shiftKey) { return hterm.Keyboard.KeyActions.PASS; + } return '\x1b[2~'; }; /** * Either scroll the scrollback buffer or send a key sequence. + * + * @param {!KeyboardEvent} e The event to process. + * @return {symbol|string} Key action or sequence. */ hterm.Keyboard.KeyMap.prototype.onKeyHome_ = function(e) { - if (!this.keyboard.homeKeysScroll ^ e.shiftKey) { - if ((e.altey || e.ctrlKey || e.shiftKey) || + if (this.keyboard.homeKeysScroll === e.shiftKey) { + if ((e.altKey || e.ctrlKey || e.shiftKey) || !this.keyboard.applicationCursor) { return '\x1b[H'; } @@ -7189,9 +8567,12 @@ hterm.Keyboard.KeyMap.prototype.onKeyHome_ = function(e) { /** * Either scroll the scrollback buffer or send a key sequence. + * + * @param {!KeyboardEvent} e The event to process. + * @return {symbol|string} Key action or sequence. */ hterm.Keyboard.KeyMap.prototype.onKeyEnd_ = function(e) { - if (!this.keyboard.homeKeysScroll ^ e.shiftKey) { + if (this.keyboard.homeKeysScroll === e.shiftKey) { if ((e.altKey || e.ctrlKey || e.shiftKey) || !this.keyboard.applicationCursor) { return '\x1b[F'; @@ -7206,10 +8587,14 @@ hterm.Keyboard.KeyMap.prototype.onKeyEnd_ = function(e) { /** * Either scroll the scrollback buffer or send a key sequence. + * + * @param {!KeyboardEvent} e The event to process. + * @return {symbol|string} Key action or sequence. */ hterm.Keyboard.KeyMap.prototype.onKeyPageUp_ = function(e) { - if (!this.keyboard.pageKeysScroll ^ e.shiftKey) + if (this.keyboard.pageKeysScroll === e.shiftKey) { return '\x1b[5~'; + } this.keyboard.terminal.scrollPageUp(); return hterm.Keyboard.KeyActions.CANCEL; @@ -7222,20 +8607,28 @@ hterm.Keyboard.KeyMap.prototype.onKeyPageUp_ = function(e) { * claims that the alt key is not pressed, we know the DEL was a synthetic * one from a user that hit alt-backspace. Based on a user pref, we can sub * in meta-backspace in this case. + * + * @param {!KeyboardEvent} e The event to process. + * @return {symbol|string} Key action or sequence. */ hterm.Keyboard.KeyMap.prototype.onKeyDel_ = function(e) { if (this.keyboard.altBackspaceIsMetaBackspace && - this.keyboard.altKeyPressed && !e.altKey) + this.keyboard.altKeyPressed && !e.altKey) { return '\x1b\x7f'; + } return '\x1b[3~'; }; /** * Either scroll the scrollback buffer or send a key sequence. + * + * @param {!KeyboardEvent} e The event to process. + * @return {symbol|string} Key action or sequence. */ hterm.Keyboard.KeyMap.prototype.onKeyPageDown_ = function(e) { - if (!this.keyboard.pageKeysScroll ^ e.shiftKey) + if (this.keyboard.pageKeysScroll === e.shiftKey) { return '\x1b[6~'; + } this.keyboard.terminal.scrollPageDown(); return hterm.Keyboard.KeyActions.CANCEL; @@ -7243,6 +8636,9 @@ hterm.Keyboard.KeyMap.prototype.onKeyPageDown_ = function(e) { /** * Either scroll the scrollback buffer or send a key sequence. + * + * @param {!KeyboardEvent} e The event to process. + * @return {symbol|string} Key action or sequence. */ hterm.Keyboard.KeyMap.prototype.onKeyArrowUp_ = function(e) { if (!this.keyboard.applicationCursor && e.shiftKey) { @@ -7256,6 +8652,9 @@ hterm.Keyboard.KeyMap.prototype.onKeyArrowUp_ = function(e) { /** * Either scroll the scrollback buffer or send a key sequence. + * + * @param {!KeyboardEvent} e The event to process. + * @return {symbol|string} Key action or sequence. */ hterm.Keyboard.KeyMap.prototype.onKeyArrowDown_ = function(e) { if (!this.keyboard.applicationCursor && e.shiftKey) { @@ -7269,8 +8668,11 @@ hterm.Keyboard.KeyMap.prototype.onKeyArrowDown_ = function(e) { /** * Clear the primary/alternate screens and the scrollback buffer. + * + * @param {!KeyboardEvent} e The event to process. + * @return {symbol|string} Key action or sequence. */ -hterm.Keyboard.KeyMap.prototype.onClear_ = function(e, keyDef) { +hterm.Keyboard.KeyMap.prototype.onClear_ = function(e) { this.keyboard.terminal.wipeContents(); return hterm.Keyboard.KeyActions.CANCEL; }; @@ -7281,27 +8683,36 @@ hterm.Keyboard.KeyMap.prototype.onClear_ = function(e, keyDef) { * It would be nice to use the Fullscreen API, but the UX is slightly different * a bad way: the Escape key is automatically registered for exiting. If we let * the browser handle F11 directly though, we still get to capture Escape. + * + * @param {!KeyboardEvent} e The event to process. + * @return {symbol|string} Key action or sequence. */ -hterm.Keyboard.KeyMap.prototype.onF11_ = function(e, keyDef) { - if (hterm.windowType != 'popup') +hterm.Keyboard.KeyMap.prototype.onF11_ = function(e) { + if (hterm.windowType != 'popup' && !e.shiftKey) { return hterm.Keyboard.KeyActions.PASS; - else + } else { return '\x1b[23~'; + } }; /** - * Either pass Ctrl-1..9 to the browser or send them to the host. + * Either pass Ctrl+1..9 to the browser or send them to the host. * - * Note that Ctrl-1 and Ctrl-9 don't actually have special sequences mapped - * to them in xterm or gnome-terminal. The range is really Ctrl-2..8, but + * Note that Ctrl+1 and Ctrl+9 don't actually have special sequences mapped + * to them in xterm or gnome-terminal. The range is really Ctrl+2..8, but * we handle 1..9 since Chrome treats the whole range special. + * + * @param {!KeyboardEvent} e The event to process. + * @param {!hterm.Keyboard.KeyDef} keyDef Key definition. + * @return {symbol|string} Key action or sequence. */ hterm.Keyboard.KeyMap.prototype.onCtrlNum_ = function(e, keyDef) { // Compute a control character for a given character. function ctl(ch) { return String.fromCharCode(ch.charCodeAt(0) - 64); } - if (this.keyboard.terminal.passCtrlNumber && !e.shiftKey) + if (this.keyboard.terminal.passCtrlNumber && !e.shiftKey) { return hterm.Keyboard.KeyActions.PASS; + } switch (keyDef.keyCap.substr(0, 1)) { case '1': return '1'; @@ -7314,38 +8725,102 @@ hterm.Keyboard.KeyMap.prototype.onCtrlNum_ = function(e, keyDef) { case '8': return '\x7f'; case '9': return '9'; } + return hterm.Keyboard.KeyActions.PASS; }; /** - * Either pass Alt-1..9 to the browser or send them to the host. + * Either pass Alt+1..9 to the browser or send them to the host. + * + * @param {!KeyboardEvent} e The event to process. + * @return {symbol|string} Key action or sequence. */ -hterm.Keyboard.KeyMap.prototype.onAltNum_ = function(e, keyDef) { - if (this.keyboard.terminal.passAltNumber && !e.shiftKey) +hterm.Keyboard.KeyMap.prototype.onAltNum_ = function(e) { + if (this.keyboard.terminal.passAltNumber && !e.shiftKey) { return hterm.Keyboard.KeyActions.PASS; + } return hterm.Keyboard.KeyActions.DEFAULT; }; /** - * Either pass Meta-1..9 to the browser or send them to the host. + * Either pass Meta+1..9 to the browser or send them to the host. + * + * @param {!KeyboardEvent} e The event to process. + * @return {symbol|string} Key action or sequence. */ -hterm.Keyboard.KeyMap.prototype.onMetaNum_ = function(e, keyDef) { - if (this.keyboard.terminal.passMetaNumber && !e.shiftKey) +hterm.Keyboard.KeyMap.prototype.onMetaNum_ = function(e) { + if (this.keyboard.terminal.passMetaNumber && !e.shiftKey) { return hterm.Keyboard.KeyActions.PASS; + } return hterm.Keyboard.KeyActions.DEFAULT; }; +/** + * Either pass ctrl+[shift]+tab to the browser or strip. + * + * @param {!KeyboardEvent} e The event to process. + * @return {symbol|string} Key action or sequence. + */ +hterm.Keyboard.KeyMap.prototype.onCtrlTab_ = function(e) { + if (this.keyboard.terminal.passCtrlTab) { + return hterm.Keyboard.KeyActions.PASS; + } + return hterm.Keyboard.KeyActions.STRIP; +}; + +/** + * Either pass Ctrl & Shift W (close tab/window) to the browser or send it to + * the host. + * + * @param {!KeyboardEvent} e The event to process. + * @return {symbol|string} Key action or sequence. + */ +hterm.Keyboard.KeyMap.prototype.onCtrlW_ = function(e) { + if (this.keyboard.terminal.passCtrlW) { + return hterm.Keyboard.KeyActions.PASS; + } + return '\x17'; +}; + +/** + * Either pass Ctrl & Shift T (new/reopen tab) to the browser or send it to the + * host. + * + * @param {!KeyboardEvent} e The event to process. + * @return {symbol|string} Key action or sequence. + */ +hterm.Keyboard.KeyMap.prototype.onCtrlT_ = function(e) { + if (this.keyboard.terminal.passCtrlT) { + return hterm.Keyboard.KeyActions.PASS; + } + return '\x14'; +}; + +/** + * Display the find bar. + * + * @param {!KeyboardEvent} e The event to process. + * @return {symbol} Key action or sequence. + */ +hterm.Keyboard.KeyMap.prototype.onCtrlShiftF_ = function(e) { + this.keyboard.terminal.findBar.display(); + return hterm.Keyboard.KeyActions.CANCEL; +}; + /** * Either send a ^C or interpret the keystroke as a copy command. + * + * @param {!KeyboardEvent} e The event to process. + * @return {symbol|string} Key action or sequence. */ -hterm.Keyboard.KeyMap.prototype.onCtrlC_ = function(e, keyDef) { - var selection = this.keyboard.terminal.getDocument().getSelection(); +hterm.Keyboard.KeyMap.prototype.onCtrlC_ = function(e) { + const selection = this.keyboard.terminal.getDocument().getSelection(); if (!selection.isCollapsed) { if (this.keyboard.ctrlCCopy && !e.shiftKey) { - // Ctrl-C should copy if there is a selection, send ^C otherwise. - // Perform the copy by letting the browser handle Ctrl-C. On most + // Ctrl+C should copy if there is a selection, send ^C otherwise. + // Perform the copy by letting the browser handle Ctrl+C. On most // browsers, this is the *only* way to place text on the clipboard from // the 'drive-by' web. if (this.keyboard.terminal.clearSelectionAfterCopy) { @@ -7355,7 +8830,7 @@ hterm.Keyboard.KeyMap.prototype.onCtrlC_ = function(e, keyDef) { } if (!this.keyboard.ctrlCCopy && e.shiftKey) { - // Ctrl-Shift-C should copy if there is a selection, send ^C otherwise. + // Ctrl+Shift+C should copy if there is a selection, send ^C otherwise. // Perform the copy manually. This only works in situations where // document.execCommand('copy') is allowed. if (this.keyboard.terminal.clearSelectionAfterCopy) { @@ -7371,14 +8846,24 @@ hterm.Keyboard.KeyMap.prototype.onCtrlC_ = function(e, keyDef) { /** * Either send a ^N or open a new window to the same location. + * + * @param {!KeyboardEvent} e The event to process. + * @return {!hterm.Keyboard.KeyDefFunction|symbol|string} Key action or + * sequence. */ -hterm.Keyboard.KeyMap.prototype.onCtrlN_ = function(e, keyDef) { +hterm.Keyboard.KeyMap.prototype.onCtrlN_ = function(e) { + if (this.keyboard.terminal.passCtrlN) { + return hterm.Keyboard.KeyActions.PASS; + } + if (e.shiftKey) { - lib.f.openWindow(document.location.href, '', - 'chrome=no,close=yes,resize=yes,scrollbars=yes,' + - 'minimizable=yes,width=' + window.innerWidth + - ',height=' + window.innerHeight); - return hterm.Keyboard.KeyActions.CANCEL; + return function(e, k) { + lib.f.openWindow(document.location.href, '', + 'chrome=no,close=yes,resize=yes,scrollbars=yes,' + + 'minimizable=yes,width=' + window.innerWidth + + ',height=' + window.innerHeight); + return hterm.Keyboard.KeyActions.CANCEL; + }; } return '\x0e'; @@ -7387,22 +8872,25 @@ hterm.Keyboard.KeyMap.prototype.onCtrlN_ = function(e, keyDef) { /** * Either send a ^V or issue a paste command. * - * The default behavior is to paste if the user presses Ctrl-Shift-V, and send - * a ^V if the user presses Ctrl-V. This can be flipped with the + * The default behavior is to paste if the user presses Ctrl+Shift+V, and send + * a ^V if the user presses Ctrl+V. This can be flipped with the * 'ctrl-v-paste' preference. * + * @param {!KeyboardEvent} e The event to process. + * @return {symbol|string} Key action or sequence. */ -hterm.Keyboard.KeyMap.prototype.onCtrlV_ = function(e, keyDef) { +hterm.Keyboard.KeyMap.prototype.onCtrlV_ = function(e) { if ((!e.shiftKey && this.keyboard.ctrlVPaste) || (e.shiftKey && !this.keyboard.ctrlVPaste)) { - // We try to do the pasting ourselves as not all browsers/OSs bind Ctrl-V to - // pasting. Notably, on macOS, Ctrl-V/Ctrl-Shift-V do nothing. + // We try to do the pasting ourselves as not all browsers/OSs bind Ctrl+V to + // pasting. Notably, on macOS, Ctrl+V/Ctrl+Shift+V do nothing. // However, this might run into web restrictions, so if it fails, we still // fallback to the letting the native behavior (hopefully) save us. - if (this.keyboard.terminal.paste()) + if (this.keyboard.terminal.paste() !== false) { return hterm.Keyboard.KeyActions.CANCEL; - else + } else { return hterm.Keyboard.KeyActions.PASS; + } } return '\x16'; @@ -7410,35 +8898,44 @@ hterm.Keyboard.KeyMap.prototype.onCtrlV_ = function(e, keyDef) { /** * Either the default action or open a new window to the same location. + * + * @param {!KeyboardEvent} e The event to process. + * @return {!hterm.Keyboard.KeyDefFunction|symbol} Key action or sequence. */ -hterm.Keyboard.KeyMap.prototype.onMetaN_ = function(e, keyDef) { +hterm.Keyboard.KeyMap.prototype.onMetaN_ = function(e) { if (e.shiftKey) { - lib.f.openWindow(document.location.href, '', - 'chrome=no,close=yes,resize=yes,scrollbars=yes,' + - 'minimizable=yes,width=' + window.outerWidth + - ',height=' + window.outerHeight); - return hterm.Keyboard.KeyActions.CANCEL; + return function(e, k) { + lib.f.openWindow(document.location.href, '', + 'chrome=no,close=yes,resize=yes,scrollbars=yes,' + + 'minimizable=yes,width=' + window.outerWidth + + ',height=' + window.outerHeight); + return hterm.Keyboard.KeyActions.CANCEL; + }; } return hterm.Keyboard.KeyActions.DEFAULT; }; /** - * Either send a Meta-C or allow the browser to interpret the keystroke as a + * Either send a Meta+C or allow the browser to interpret the keystroke as a * copy command. * - * If there is no selection, or if the user presses Meta-Shift-C, then we'll + * If there is no selection, or if the user presses Meta+Shift+C, then we'll * transmit an '\x1b' (if metaSendsEscape is on) followed by 'c' or 'C'. * * If there is a selection, we defer to the browser. In this case we clear out * the selection so the user knows we heard them, and also to give them a - * chance to send a Meta-C by just hitting the key again. + * chance to send a Meta+C by just hitting the key again. + * + * @param {!KeyboardEvent} e The event to process. + * @param {!hterm.Keyboard.KeyDef} keyDef Key definition. + * @return {symbol|string} Key action or sequence. */ hterm.Keyboard.KeyMap.prototype.onMetaC_ = function(e, keyDef) { - var document = this.keyboard.terminal.getDocument(); + const document = this.keyboard.terminal.getDocument(); if (e.shiftKey || document.getSelection().isCollapsed) { // If the shift key is being held, or there is no document selection, send - // a Meta-C. The keyboard code will add the ESC if metaSendsEscape is true, + // a Meta+C. The keyboard code will add the ESC if metaSendsEscape is true, // we just have to decide between 'c' and 'C'. return keyDef.keyCap.substr(e.shiftKey ? 1 : 0, 1); } @@ -7451,14 +8948,18 @@ hterm.Keyboard.KeyMap.prototype.onMetaC_ = function(e, keyDef) { }; /** - * Either PASS or DEFAULT Meta-V, depending on preference. + * Either PASS or DEFAULT Meta+V, depending on preference. * - * Always PASS Meta-Shift-V to allow browser to interpret the keystroke as + * Always PASS Meta+Shift+V to allow browser to interpret the keystroke as * a paste command. + * + * @param {!KeyboardEvent} e The event to process. + * @return {symbol|string} Key action or sequence. */ -hterm.Keyboard.KeyMap.prototype.onMetaV_ = function(e, keyDef) { - if (e.shiftKey) +hterm.Keyboard.KeyMap.prototype.onMetaV_ = function(e) { + if (e.shiftKey) { return hterm.Keyboard.KeyActions.PASS; + } return this.keyboard.passMetaV ? hterm.Keyboard.KeyActions.PASS : @@ -7468,36 +8969,29 @@ hterm.Keyboard.KeyMap.prototype.onMetaV_ = function(e, keyDef) { /** * Handle font zooming. * - * The browser's built-in zoom has a bit of an issue at certain zoom levels. - * At some magnifications, the measured height of a row of text differs from - * the height that was explicitly set. - * - * We override the browser zoom keys to change the ScrollPort's font size to - * avoid the issue. + * @param {!KeyboardEvent} e The event to process. + * @param {!hterm.Keyboard.KeyDef} keyDef Key definition. + * @return {symbol|string} Key action or sequence. */ -hterm.Keyboard.KeyMap.prototype.onPlusMinusZero_ = function(e, keyDef) { - if (!(this.keyboard.ctrlPlusMinusZeroZoom ^ e.shiftKey)) { +hterm.Keyboard.KeyMap.prototype.onZoom_ = function(e, keyDef) { + if (this.keyboard.ctrlPlusMinusZeroZoom === e.shiftKey) { // If ctrl-PMZ controls zoom and the shift key is pressed, or // ctrl-shift-PMZ controls zoom and this shift key is not pressed, // then we want to send the control code instead of affecting zoom. - if (keyDef.keyCap == '-_') - return '\x1f'; // ^_ + if (keyDef.keyCap == '-_') { + // ^_ + return '\x1f'; + } // Only ^_ is valid, the other sequences have no meaning. return hterm.Keyboard.KeyActions.CANCEL; } - if (this.keyboard.terminal.getZoomFactor() != 1) { - // If we're not at 1:1 zoom factor, let the Ctrl +/-/0 keys control the - // browser zoom, so it's easier to for the user to get back to 100%. - return hterm.Keyboard.KeyActions.PASS; - } - - var cap = keyDef.keyCap.substr(0, 1); + const cap = keyDef.keyCap.substr(0, 1); if (cap == '0') { this.keyboard.terminal.setFontSize(0); } else { - var size = this.keyboard.terminal.getFontSize(); + let size = this.keyboard.terminal.getFontSize(); if (cap == '-' || keyDef.keyCap == '[KP-]') { size -= 1; @@ -7522,6 +9016,9 @@ hterm.Keyboard.KeyMap.prototype.onPlusMinusZero_ = function(e, keyDef) { * property below. Each modifier can be true, false, or "*". True means * the modifier key must be present, false means it must not, and "*" means * it doesn't matter. + * + * @param {!hterm.Keyboard.KeyDown} spec + * @constructor */ hterm.Keyboard.KeyPattern = function(spec) { this.wildcardCount = 0; @@ -7529,8 +9026,9 @@ hterm.Keyboard.KeyPattern = function(spec) { hterm.Keyboard.KeyPattern.modifiers.forEach(function(mod) { this[mod] = spec[mod] || false; - if (this[mod] == '*') + if (this[mod] == '*') { this.wildcardCount++; + } }.bind(this)); }; @@ -7538,7 +9036,7 @@ hterm.Keyboard.KeyPattern = function(spec) { * Valid modifier names. */ hterm.Keyboard.KeyPattern.modifiers = [ - 'shift', 'ctrl', 'alt', 'meta' + 'shift', 'ctrl', 'alt', 'meta', ]; /** @@ -7548,15 +9046,18 @@ hterm.Keyboard.KeyPattern.modifiers = [ * patterns first, so that loosely defined patterns have a lower priority than * exact patterns. * - * @param {hterm.Keyboard.KeyPattern} a - * @param {hterm.Keyboard.KeyPattern} b + * @param {!hterm.Keyboard.KeyPattern} a + * @param {!hterm.Keyboard.KeyPattern} b + * @return {number} */ hterm.Keyboard.KeyPattern.sortCompare = function(a, b) { - if (a.wildcardCount < b.wildcardCount) + if (a.wildcardCount < b.wildcardCount) { return -1; + } - if (a.wildcardCount > b.wildcardCount) + if (a.wildcardCount > b.wildcardCount) { return 1; + } return 0; }; @@ -7565,46 +9066,179 @@ hterm.Keyboard.KeyPattern.sortCompare = function(a, b) { * Private method used to match this key pattern against other key patterns * or key down events. * - * @param {Object} The object to match. - * @param {boolean} True if we should ignore wildcards. Useful when you want + * @param {!hterm.Keyboard.KeyDown} obj The object to match. + * @param {boolean} exactMatch True if we should ignore wildcards. Useful when + * you want * to perform and exact match against another key pattern. + * @return {boolean} */ hterm.Keyboard.KeyPattern.prototype.match_ = function(obj, exactMatch) { - if (this.keyCode != obj.keyCode) + if (this.keyCode != obj.keyCode) { return false; + } + + let rv = true; + + hterm.Keyboard.KeyPattern.modifiers.forEach(function(mod) { + const modValue = (mod in obj) ? obj[mod] : false; + if (!rv || (!exactMatch && this[mod] == '*') || this[mod] == modValue) { + return; + } + + rv = false; + }.bind(this)); + + return rv; +}; + +/** + * Return true if the given keyDown object is a match for this key pattern. + * + * @param {!hterm.Keyboard.KeyDown} keyDown An object with a keyCode property + * and zero or more boolean properties representing key modifiers. These + * property names must match those defined in + * hterm.Keyboard.KeyPattern.modifiers. + * @return {boolean} + */ +hterm.Keyboard.KeyPattern.prototype.matchKeyDown = function(keyDown) { + return this.match_(keyDown, false); +}; + +/** + * Return true if the given hterm.Keyboard.KeyPattern is exactly the same as + * this one. + * + * @param {!hterm.Keyboard.KeyPattern} keyPattern + * @return {boolean} + */ +hterm.Keyboard.KeyPattern.prototype.matchKeyPattern = function(keyPattern) { + return this.match_(keyPattern, true); +}; +// SOURCE FILE: hterm/js/hterm_notifications.js +// Copyright 2020 The Chromium OS 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 A UI for managing user notifications. It's a distinct UI space + * from the terminal itself to help users clearly distinguish between remote + * output. This makes it hard for the remote to spoof the user. + */ + +/** + * Class that controls everything about the notification center. + */ +hterm.NotificationCenter = class { + /** + * @param {!Element} parent The node that we will display inside. + * @param {?hterm.AccessibilityReader=} reader Helper for reading content. + */ + constructor(parent, reader = undefined) { + this.parent_ = parent; + this.reader_ = reader; + this.container_ = this.newContainer_(); + /** @type {?number} Id for automatic hiding timeout. */ + this.timeout_ = null; + /** @type {number} Fadeout delay (for tests to control). */ + this.fadeout_ = 200; + } + + /** @return {!Element} */ + newContainer_() { + const ele = this.parent_.ownerDocument.createElement('div'); + ele.style.cssText = + 'color: rgb(var(--hterm-background-color));' + + 'background-color: rgb(var(--hterm-foreground-color));' + + 'border-radius: 12px;' + + 'font: 500 var(--hterm-font-size) "Noto Sans", sans-serif;' + + 'opacity: 0.75;' + + 'padding: 0.923em 1.846em;' + + 'position: absolute;' + + 'user-select: none;' + + 'transition: opacity 180ms ease-in;'; + + // Prevent the dialog from gaining focus. + ele.addEventListener('mousedown', function(e) { + e.preventDefault(); + e.stopPropagation(); + }, true); - var rv = true; + return ele; + } - hterm.Keyboard.KeyPattern.modifiers.forEach(function(mod) { - var modValue = (mod in obj) ? obj[mod] : false; - if (!rv || (!exactMatch && this[mod] == '*') || this[mod] == modValue) + /** + * Show a notification for the specified duration. + * + * The notification appears in inverse video, centered over the terminal. + * + * @param {string|!Node} msg The message to display. + * @param {{ + * timeout: (?number|undefined), + * }=} options + * timeout: How long (millisec) to wait before hiding the notification. + * Pass null to never autohide. + */ + show(msg, {timeout = 1500} = {}) { + const node = typeof msg === 'string' ? new Text(msg) : msg; + + // Hacky heuristic: if we're currently showing a notification w/out a + // timeout, and the new one includes a timeout, leave the existing one + // alone. We should rework this stack a bit to give more power to the + // callers, but for now, this should be OK. + if (this.container_.parentNode && this.timeout_ === null && + timeout !== null) { return; + } - rv = false; - }.bind(this)); + // Remove all children first. + this.container_.textContent = ''; + this.container_.appendChild(node); + this.container_.style.opacity = '0.75'; - return rv; -}; + // Display on the page if it isn't already. + if (!this.container_.parentNode) { + this.parent_.appendChild(this.container_); + } -/** - * Return true if the given keyDown object is a match for this key pattern. - * - * @param {Object} keyDown An object with a keyCode property and zero or - * more boolean properties representing key modifiers. These property names - * must match those defined in hterm.Keyboard.KeyPattern.modifiers. - */ -hterm.Keyboard.KeyPattern.prototype.matchKeyDown = function(keyDown) { - return this.match_(keyDown, false); -}; + // Keep the notification centered. + const size = this.container_.getBoundingClientRect(); + this.container_.style.top = `calc(50% - ${size.height / 2}px)`; + this.container_.style.left = `calc(50% - ${size.width / 2}px)`; -/** - * Return true if the given hterm.Keyboard.KeyPattern is exactly the same as - * this one. - * - * @param {hterm.Keyboard.KeyPattern} - */ -hterm.Keyboard.KeyPattern.prototype.matchKeyPattern = function(keyPattern) { - return this.match_(keyPattern, true); + if (this.reader_) { + this.reader_.assertiveAnnounce(this.container_.textContent); + } + + // Handle automatic hiding of the UI. + if (this.timeout_) { + clearTimeout(this.timeout_); + this.timeout_ = null; + } + if (timeout === null) { + return; + } + this.timeout_ = setTimeout(() => { + this.container_.style.opacity = '0'; + this.timeout_ = setTimeout(() => this.hide(), this.fadeout_); + }, timeout); + } + + /** + * Hide the active notification immediately. + * + * Useful when we show a message for an event with an unknown end time. + */ + hide() { + if (this.timeout_) { + clearTimeout(this.timeout_); + this.timeout_ = null; + } + + this.container_.remove(); + // Remove all children in case there was sensitive content shown that we + // don't want to leave laying around. + this.container_.textContent = ''; + } }; // SOURCE FILE: hterm/js/hterm_options.js // Copyright (c) 2012 The Chromium OS Authors. All rights reserved. @@ -7629,22 +9263,22 @@ hterm.Keyboard.KeyPattern.prototype.matchKeyPattern = function(keyPattern) { * except that we enable autowrap (wraparound) by default since that seems to * be what xterm does. * - * @param {hterm.Options=} opt_copy Optional instance to copy. + * @param {!hterm.Options=} copy Optional instance to copy. * @constructor */ -hterm.Options = function(opt_copy) { +hterm.Options = function(copy = undefined) { // All attributes in this class are public to allow easy access by the // terminal. - this.wraparound = opt_copy ? opt_copy.wraparound : true; - this.reverseWraparound = opt_copy ? opt_copy.reverseWraparound : false; - this.originMode = opt_copy ? opt_copy.originMode : false; - this.autoCarriageReturn = opt_copy ? opt_copy.autoCarriageReturn : false; - this.cursorVisible = opt_copy ? opt_copy.cursorVisible : false; - this.cursorBlink = opt_copy ? opt_copy.cursorBlink : false; - this.insertMode = opt_copy ? opt_copy.insertMode : false; - this.reverseVideo = opt_copy ? opt_copy.reverseVideo : false; - this.bracketedPaste = opt_copy ? opt_copy.bracketedPaste : false; + this.wraparound = copy ? copy.wraparound : true; + this.reverseWraparound = copy ? copy.reverseWraparound : false; + this.originMode = copy ? copy.originMode : false; + this.autoCarriageReturn = copy ? copy.autoCarriageReturn : false; + this.cursorVisible = copy ? copy.cursorVisible : false; + this.cursorBlink = copy ? copy.cursorBlink : false; + this.insertMode = copy ? copy.insertMode : false; + this.reverseVideo = copy ? copy.reverseVideo : false; + this.bracketedPaste = copy ? copy.bracketedPaste : false; }; // SOURCE FILE: hterm/js/hterm_parser.js // Copyright (c) 2015 The Chromium OS Authors. All rights reserved. @@ -7652,8 +9286,9 @@ hterm.Options = function(opt_copy) { // found in the LICENSE file. /** - * @constructor * Parses the key definition syntax used for user keyboard customizations. + * + * @constructor */ hterm.Parser = function() { /** @@ -7667,22 +9302,31 @@ hterm.Parser = function() { this.pos = 0; /** - * @type {string?} The character at the current position. + * @type {?string} The character at the current position. */ this.ch = null; }; +/** + * @param {string} message + * @return {!Error} + */ hterm.Parser.prototype.error = function(message) { return new Error('Parse error at ' + this.pos + ': ' + message); }; +/** @return {boolean} */ hterm.Parser.prototype.isComplete = function() { return this.pos == this.source.length; }; -hterm.Parser.prototype.reset = function(source, opt_pos) { +/** + * @param {string} source + * @param {number=} pos + */ +hterm.Parser.prototype.reset = function(source, pos = 0) { this.source = source; - this.pos = opt_pos || 0; + this.pos = pos; this.ch = source.substr(0, 1); }; @@ -7702,39 +9346,40 @@ hterm.Parser.prototype.reset = function(source, opt_pos) { * A: Matches only an unmodified "A" character. * 65: Same as above. * 0x41: Same as above. - * Ctrl-A: Matches only Ctrl-A. - * Ctrl-65: Same as above. - * Ctrl-0x41: Same as above. - * Ctrl-Shift-A: Matches only Ctrl-Shift-A. - * Ctrl-*-A: Matches Ctrl-A, as well as any other key sequence that includes + * Ctrl+A: Matches only Ctrl+A. + * Ctrl+65: Same as above. + * Ctrl+0x41: Same as above. + * Ctrl+Shift+A: Matches only Ctrl+Shift+A. + * Ctrl+*+A: Matches Ctrl+A, as well as any other key sequence that includes * at least the Ctrl and A keys. * - * @return {Object} An object with shift, ctrl, alt, meta, keyCode - * properties. + * @return {!hterm.Keyboard.KeyDown} An object with shift, ctrl, alt, meta, + * keyCode properties. */ hterm.Parser.prototype.parseKeySequence = function() { - var rv = { - keyCode: null + const rv = { + keyCode: null, }; - for (var k in hterm.Parser.identifiers.modifierKeys) { + for (const k in hterm.Parser.identifiers.modifierKeys) { rv[hterm.Parser.identifiers.modifierKeys[k]] = false; } while (this.pos < this.source.length) { this.skipSpace(); - var token = this.parseToken(); + const token = this.parseToken(); if (token.type == 'integer') { rv.keyCode = token.value; } else if (token.type == 'identifier') { - var ucValue = token.value.toUpperCase(); + const ucValue = token.value.toUpperCase(); if (ucValue in hterm.Parser.identifiers.modifierKeys && hterm.Parser.identifiers.modifierKeys.hasOwnProperty(ucValue)) { - var mod = hterm.Parser.identifiers.modifierKeys[ucValue]; - if (rv[mod] && rv[mod] != '*') + const mod = hterm.Parser.identifiers.modifierKeys[ucValue]; + if (rv[mod] && rv[mod] != '*') { throw this.error('Duplicate modifier: ' + token.value); + } rv[mod] = true; } else if (ucValue in hterm.Parser.identifiers.keyCodes && @@ -7747,10 +9392,11 @@ hterm.Parser.prototype.parseKeySequence = function() { } else if (token.type == 'symbol') { if (token.value == '*') { - for (var id in hterm.Parser.identifiers.modifierKeys) { - var p = hterm.Parser.identifiers.modifierKeys[id]; - if (!rv[p]) - rv[p] = '*'; + for (const id in hterm.Parser.identifiers.modifierKeys) { + const p = hterm.Parser.identifiers.modifierKeys[id]; + if (!rv[p]) { + rv[p] = '*'; + } } } else { throw this.error('Unexpected symbol: ' + token.value); @@ -7761,33 +9407,39 @@ hterm.Parser.prototype.parseKeySequence = function() { this.skipSpace(); - if (this.ch != '-') + if (this.ch !== '-' && this.ch !== '+') { break; + } - if (rv.keyCode != null) + if (rv.keyCode != null) { throw this.error('Extra definition after target key'); + } this.advance(1); } - if (rv.keyCode == null) + if (rv.keyCode == null) { throw this.error('Missing target key'); + } return rv; }; +/** @return {string} */ hterm.Parser.prototype.parseKeyAction = function() { this.skipSpace(); - var token = this.parseToken(); + const token = this.parseToken(); - if (token.type == 'string') + if (token.type == 'string') { return token.value; + } if (token.type == 'identifier') { if (token.value in hterm.Parser.identifiers.actions && - hterm.Parser.identifiers.actions.hasOwnProperty(token.value)) + hterm.Parser.identifiers.actions.hasOwnProperty(token.value)) { return hterm.Parser.identifiers.actions[token.value]; + } throw this.error('Unknown key action: ' + token.value); } @@ -7796,54 +9448,62 @@ hterm.Parser.prototype.parseKeyAction = function() { }; +/** @return {boolean} */ hterm.Parser.prototype.peekString = function() { return this.ch == '\'' || this.ch == '"'; }; +/** @return {boolean} */ hterm.Parser.prototype.peekIdentifier = function() { - return this.ch.match(/[a-z_]/i); + return !!this.ch.match(/[a-z_]/i); }; +/** @return {boolean} */ hterm.Parser.prototype.peekInteger = function() { - return this.ch.match(/[0-9]/); + return !!this.ch.match(/[0-9]/); }; +/** @return {!Object} */ hterm.Parser.prototype.parseToken = function() { if (this.ch == '*') { - var rv = {type: 'symbol', value: this.ch}; + const rv = {type: 'symbol', value: this.ch}; this.advance(1); return rv; } - if (this.peekIdentifier()) + if (this.peekIdentifier()) { return {type: 'identifier', value: this.parseIdentifier()}; + } - if (this.peekString()) + if (this.peekString()) { return {type: 'string', value: this.parseString()}; + } - if (this.peekInteger()) + if (this.peekInteger()) { return {type: 'integer', value: this.parseInteger()}; - + } throw this.error('Unexpected token'); }; +/** @return {string} */ hterm.Parser.prototype.parseIdentifier = function() { - if (!this.peekIdentifier()) + if (!this.peekIdentifier()) { throw this.error('Expected identifier'); + } return this.parsePattern(/[a-z0-9_]+/ig); }; +/** @return {number} */ hterm.Parser.prototype.parseInteger = function() { - var base = 10; - if (this.ch == '0' && this.pos < this.source.length - 1 && this.source.substr(this.pos + 1, 1) == 'x') { - return parseInt(this.parsePattern(/0x[0-9a-f]+/gi)); + /* eslint-disable radix */ + return parseInt(this.parsePattern(/0x[0-9a-f]+/gi), undefined); } - return parseInt(this.parsePattern(/\d+/g)); + return parseInt(this.parsePattern(/\d+/g), 10); }; /** @@ -7854,25 +9514,25 @@ hterm.Parser.prototype.parseInteger = function() { * * TODO(rginda): Variable interpolation. * - * @param {ParseState} parseState - * @param {string} quote A single or double-quote character. * @return {string} */ hterm.Parser.prototype.parseString = function() { - var result = ''; + let result = ''; - var quote = this.ch; - if (quote != '"' && quote != '\'') + const quote = this.ch; + if (quote != '"' && quote != '\'') { throw this.error('String expected'); + } this.advance(1); - var re = new RegExp('[\\\\' + quote + ']', 'g'); + const re = new RegExp('[\\\\' + quote + ']', 'g'); while (this.pos < this.source.length) { re.lastIndex = this.pos; - if (!re.exec(this.source)) + if (!re.exec(this.source)) { throw this.error('Unterminated string literal'); + } result += this.source.substring(this.pos, re.lastIndex - 1); @@ -7907,7 +9567,7 @@ hterm.Parser.prototype.parseString = function() { * @return {string} */ hterm.Parser.prototype.parseEscape = function() { - var map = { + const map = { '"': '"', '\'': '\'', '\\': '\\', @@ -7920,23 +9580,25 @@ hterm.Parser.prototype.parseEscape = function() { 't': '\x09', 'v': '\x0b', 'x': function() { - var value = this.parsePattern(/[a-z0-9]{2}/ig); + const value = this.parsePattern(/[a-z0-9]{2}/ig); return String.fromCharCode(parseInt(value, 16)); }, 'u': function() { - var value = this.parsePattern(/[a-z0-9]{4}/ig); + const value = this.parsePattern(/[a-z0-9]{4}/ig); return String.fromCharCode(parseInt(value, 16)); - } + }, }; - if (!(this.ch in map && map.hasOwnProperty(this.ch))) + if (!(this.ch in map && map.hasOwnProperty(this.ch))) { throw this.error('Unknown escape: ' + this.ch); + } - var value = map[this.ch]; + let value = map[this.ch]; this.advance(1); - if (typeof value == 'function') + if (typeof value == 'function') { value = value.call(this); + } return value; }; @@ -7944,19 +9606,21 @@ hterm.Parser.prototype.parseEscape = function() { /** * Parse the given pattern starting from the current position. * - * @param {RegExp} pattern A pattern representing the characters to span. MUST + * @param {!RegExp} pattern A pattern representing the characters to span. MUST * include the "global" RegExp flag. * @return {string} */ hterm.Parser.prototype.parsePattern = function(pattern) { - if (!pattern.global) + if (!pattern.global) { throw this.error('Internal error: Span patterns must be global'); + } pattern.lastIndex = this.pos; - var ary = pattern.exec(this.source); + const ary = pattern.exec(this.source); - if (!ary || pattern.lastIndex - ary[0].length != this.pos) + if (!ary || pattern.lastIndex - ary[0].length != this.pos) { throw this.error('Expected match for: ' + pattern); + } this.pos = pattern.lastIndex - 1; this.advance(1); @@ -7976,27 +9640,28 @@ hterm.Parser.prototype.advance = function(count) { }; /** - * @param {string=} opt_expect A list of valid non-whitespace characters to + * @param {string=} expect A list of valid non-whitespace characters to * terminate on. * @return {void} */ -hterm.Parser.prototype.skipSpace = function(opt_expect) { - if (!/\s/.test(this.ch)) +hterm.Parser.prototype.skipSpace = function(expect = undefined) { + if (!/\s/.test(this.ch)) { return; + } - var re = /\s+/gm; + const re = /\s+/gm; re.lastIndex = this.pos; - var source = this.source; - if (re.exec(source)) + const source = this.source; + if (re.exec(source)) { this.pos = re.lastIndex; + } this.ch = this.source.substr(this.pos, 1); - if (opt_expect) { - if (this.ch.indexOf(opt_expect) == -1) { - throw this.error('Expected one of ' + opt_expect + ', found: ' + - this.ch); + if (expect) { + if (this.ch.indexOf(expect) == -1) { + throw this.error(`Expected one of ${expect}, found: ${this.ch}`); } } }; @@ -8014,7 +9679,7 @@ hterm.Parser.identifiers = {}; * Modifier key names used when defining key sequences. * * These are upper case so we can normalize the user input and be forgiving. - * "CTRL-A" and "Ctrl-A" and "ctrl-a" are all accepted. + * "CTRL+A" and "Ctrl+A" and "ctrl+a" are all accepted. * * Note: Names here cannot overlap with hterm.Parser.identifiers.keyCodes. */ @@ -8024,7 +9689,7 @@ hterm.Parser.identifiers.modifierKeys = { // Common alias. CONTROL: 'ctrl', ALT: 'alt', - META: 'meta' + META: 'meta', }; /** @@ -8033,11 +9698,11 @@ hterm.Parser.identifiers.modifierKeys = { * Punctuation is mostly left out of this list because they can move around * based on keyboard locale and browser. * - * In a key sequence like "Ctrl-ESC", the ESC comes from this list of - * identifiers. It is equivalent to "Ctrl-27" and "Ctrl-0x1b". + * In a key sequence like "Ctrl+ESC", the ESC comes from this list of + * identifiers. It is equivalent to "Ctrl+27" and "Ctrl+0x1b". * * These are upper case so we can normalize the user input and be forgiving. - * "Ctrl-ESC" and "Ctrl-Esc" an "Ctrl-esc" are all accepted. + * "Ctrl+ESC" and "Ctrl+Esc" an "Ctrl+esc" are all accepted. * * We also include common aliases for the same key. "Esc" and "Escape" are the * same key. @@ -8072,6 +9737,8 @@ hterm.Parser.identifiers.keyCodes = { EIGHT: 56, NINE: 57, ZERO: 48, + MINUS: 189, + EQUAL: 187, BACKSPACE: 8, BKSP: 8, BS: 8, @@ -8085,9 +9752,12 @@ hterm.Parser.identifiers.keyCodes = { T: 84, Y: 89, U: 85, + // eslint-disable-next-line id-blacklist I: 73, O: 79, P: 80, + BRACKET_LEFT: 219, + BRACKET_RIGHT: 221, // Row four. CAPS_LOCK: 20, @@ -8174,7 +9844,7 @@ hterm.Parser.identifiers.keyCodes = { FULL_SCREEN: 183, WINDOW_OVERVIEW: 182, BRIGHTNESS_UP: 216, - BRIGHTNESS_DOWN: 217 + BRIGHTNESS_DOWN: 217, }; /** @@ -8198,6 +9868,9 @@ hterm.Parser.identifiers.actions = { /** * Scroll the terminal one line up. + * + * @param {!hterm.Terminal} terminal + * @return {!hterm.Keyboard.KeyActions} */ scrollLineUp: function(terminal) { terminal.scrollLineUp(); @@ -8206,6 +9879,9 @@ hterm.Parser.identifiers.actions = { /** * Scroll the terminal one line down. + * + * @param {!hterm.Terminal} terminal + * @return {!hterm.Keyboard.KeyActions} */ scrollLineDown: function(terminal) { terminal.scrollLineDown(); @@ -8214,6 +9890,9 @@ hterm.Parser.identifiers.actions = { /** * Scroll the terminal one page up. + * + * @param {!hterm.Terminal} terminal + * @return {!hterm.Keyboard.KeyActions} */ scrollPageUp: function(terminal) { terminal.scrollPageUp(); @@ -8222,6 +9901,9 @@ hterm.Parser.identifiers.actions = { /** * Scroll the terminal one page down. + * + * @param {!hterm.Terminal} terminal + * @return {!hterm.Keyboard.KeyActions} */ scrollPageDown: function(terminal) { terminal.scrollPageDown(); @@ -8230,6 +9912,9 @@ hterm.Parser.identifiers.actions = { /** * Scroll the terminal to the top. + * + * @param {!hterm.Terminal} terminal + * @return {!hterm.Keyboard.KeyActions} */ scrollToTop: function(terminal) { terminal.scrollHome(); @@ -8238,6 +9923,9 @@ hterm.Parser.identifiers.actions = { /** * Scroll the terminal to the bottom. + * + * @param {!hterm.Terminal} terminal + * @return {!hterm.Keyboard.KeyActions} */ scrollToBottom: function(terminal) { terminal.scrollEnd(); @@ -8246,6 +9934,9 @@ hterm.Parser.identifiers.actions = { /** * Clear the active screen and move the cursor to (0,0). + * + * @param {!hterm.Terminal} terminal + * @return {!hterm.Keyboard.KeyActions} */ clearScreen: function(terminal) { terminal.clearHome(); @@ -8254,6 +9945,9 @@ hterm.Parser.identifiers.actions = { /** * Clear the scrollback buffer. + * + * @param {!hterm.Terminal} terminal + * @return {!hterm.Keyboard.KeyActions} */ clearScrollback: function(terminal) { terminal.clearScrollback(); @@ -8262,6 +9956,9 @@ hterm.Parser.identifiers.actions = { /** * Clear the terminal and scrollback buffer and move the cursor to (0,0). + * + * @param {!hterm.Terminal} terminal + * @return {!hterm.Keyboard.KeyActions} */ clearTerminal: function(terminal) { terminal.wipeContents(); @@ -8270,6 +9967,9 @@ hterm.Parser.identifiers.actions = { /** * Perform a full terminal reset. + * + * @param {!hterm.Terminal} terminal + * @return {!hterm.Keyboard.KeyActions} */ fullReset: function(terminal) { terminal.reset(); @@ -8278,6 +9978,9 @@ hterm.Parser.identifiers.actions = { /** * Perform a soft terminal reset. + * + * @param {!hterm.Terminal} terminal + * @return {!hterm.Keyboard.KeyActions} */ softReset: function(terminal) { terminal.softReset(); @@ -8293,6 +9996,10 @@ hterm.Parser.identifiers.actions = { * PreferenceManager subclass managing global NaSSH preferences. * * This is currently just an ordered list of known connection profiles. + * + * @param {string} profileId + * @extends {lib.PreferenceManager} + * @constructor */ hterm.PreferenceManager = function(profileId) { lib.PreferenceManager.call(this, hterm.defaultStorage, @@ -8311,12 +10018,13 @@ hterm.PreferenceManager.prefix_ = '/hterm/profiles/'; /** * List all the defined profiles. * - * @param {function(Array)} callback Called with the list of profiles. + * @param {!lib.Storage} storage Where to look for profiles. + * @param {function(!Array)} callback Called with the list of profiles. */ -hterm.PreferenceManager.listProfiles = function(callback) { - hterm.defaultStorage.getItems(null, (items) => { +hterm.PreferenceManager.listProfiles = function(storage, callback) { + storage.getItems(null).then((items) => { const profiles = {}; - for (let key of Object.keys(items)) { + for (const key of Object.keys(items)) { if (key.startsWith(hterm.PreferenceManager.prefix_)) { // Turn "/hterm/profiles/foo/bar/cow" to "foo/bar/cow". const subKey = key.slice(hterm.PreferenceManager.prefix_.length); @@ -8328,47 +10036,50 @@ hterm.PreferenceManager.listProfiles = function(callback) { }); }; -hterm.PreferenceManager.categories = {}; -hterm.PreferenceManager.categories.Keyboard = 'Keyboard'; -hterm.PreferenceManager.categories.Appearance = 'Appearance'; -hterm.PreferenceManager.categories.CopyPaste = 'CopyPaste'; -hterm.PreferenceManager.categories.Sounds = 'Sounds'; -hterm.PreferenceManager.categories.Scrolling = 'Scrolling'; -hterm.PreferenceManager.categories.Encoding = 'Encoding'; -hterm.PreferenceManager.categories.Extensions = 'Extensions'; -hterm.PreferenceManager.categories.Miscellaneous = 'Miscellaneous'; +/** @enum {string} */ +hterm.PreferenceManager.Categories = { + Keyboard: 'Keyboard', + Appearance: 'Appearance', + CopyPaste: 'CopyPaste', + Sounds: 'Sounds', + Scrolling: 'Scrolling', + Encoding: 'Encoding', + Extensions: 'Extensions', + Miscellaneous: 'Miscellaneous', +}; /** * List of categories, ordered by display order (top to bottom) */ hterm.PreferenceManager.categoryDefinitions = [ - { id: hterm.PreferenceManager.categories.Appearance, + {id: hterm.PreferenceManager.Categories.Appearance, text: 'Appearance (fonts, colors, images)'}, - { id: hterm.PreferenceManager.categories.CopyPaste, + {id: hterm.PreferenceManager.Categories.CopyPaste, text: 'Copy & Paste'}, - { id: hterm.PreferenceManager.categories.Encoding, + {id: hterm.PreferenceManager.Categories.Encoding, text: 'Encoding'}, - { id: hterm.PreferenceManager.categories.Keyboard, + {id: hterm.PreferenceManager.Categories.Keyboard, text: 'Keyboard'}, - { id: hterm.PreferenceManager.categories.Scrolling, + {id: hterm.PreferenceManager.Categories.Scrolling, text: 'Scrolling'}, - { id: hterm.PreferenceManager.categories.Sounds, + {id: hterm.PreferenceManager.Categories.Sounds, text: 'Sounds'}, - { id: hterm.PreferenceManager.categories.Extensions, + {id: hterm.PreferenceManager.Categories.Extensions, text: 'Extensions'}, - { id: hterm.PreferenceManager.categories.Miscellaneous, - text: 'Miscellaneous'} + {id: hterm.PreferenceManager.Categories.Miscellaneous, + text: 'Miscellaneous'}, ]; /** * Internal helper to create a default preference object. * - * @param {hterm.PreferenceManager.categories} category The pref category. * @param {string} name The user readable name/title. - * @param {Object} defaultValue The default pref value. - * @param {Object} type The type for this pref (or an array for enums). + * @param {!hterm.PreferenceManager.Categories} category The pref category. + * @param {boolean|number|string|?Object} defaultValue The default pref value. + * @param {string|!Array} type The type for this pref (or an array + * for enums). * @param {string} help The user readable help text. - * @return {Object} The default pref object. + * @return {!Object} The default pref object. */ hterm.PreferenceManager.definePref_ = function( name, category, defaultValue, type, help) { @@ -8384,36 +10095,36 @@ hterm.PreferenceManager.definePref_ = function( hterm.PreferenceManager.defaultPreferences = { 'alt-gr-mode': hterm.PreferenceManager.definePref_( 'AltGr key mode', - hterm.PreferenceManager.categories.Keyboard, + hterm.PreferenceManager.Categories.Keyboard, null, [null, 'none', 'ctrl-alt', 'left-alt', 'right-alt'], `Select an AltGr detection heuristic.\n` + `\n` + `'null': Autodetect based on navigator.language:\n` + ` 'en-us' => 'none', else => 'right-alt'\n` + - `'none': Disable any AltGr related munging.\n` + + `'none': Disable any AltGr emulation.\n` + `'ctrl-alt': Assume Ctrl+Alt means AltGr.\n` + `'left-alt': Assume left Alt means AltGr.\n` + - `'right-alt': Assume right Alt means AltGr.` + `'right-alt': Assume right Alt means AltGr.`, ), 'alt-backspace-is-meta-backspace': hterm.PreferenceManager.definePref_( - 'Alt-Backspace is Meta-Backspace', - hterm.PreferenceManager.categories.Keyboard, + 'Alt+Backspace is Meta+Backspace', + hterm.PreferenceManager.Categories.Keyboard, false, 'bool', - `If set, undoes the Chrome OS Alt-Backspace->DEL remap, so that ` + - `Alt-Backspace indeed is Alt-Backspace.` + `If set, undoes the Chrome OS Alt+Backspace->Delete remap, so that ` + + `Alt+Backspace indeed is Alt+Backspace.`, ), 'alt-is-meta': hterm.PreferenceManager.definePref_( 'Treat Alt key as Meta key', - hterm.PreferenceManager.categories.Keyboard, + hterm.PreferenceManager.Categories.Keyboard, false, 'bool', - `Whether the Alt key acts as a Meta key or as a distinct Alt key.` + `Whether the Alt key acts as a Meta key or as a distinct Alt key.`, ), 'alt-sends-what': hterm.PreferenceManager.definePref_( 'Alt key modifier handling', - hterm.PreferenceManager.categories.Keyboard, + hterm.PreferenceManager.Categories.Keyboard, 'escape', ['escape', '8-bit', 'browser-key'], `Controls how the Alt key is handled.\n` + `\n` + @@ -8421,79 +10132,79 @@ hterm.PreferenceManager.defaultPreferences = { ` 8-bit: Add 128 to the typed character as in xterm.\n` + ` browser-key: Wait for the keypress event and see what the browser\n` + ` says. (This won't work well on platforms where the browser\n` + - ` performs a default action for some Alt sequences.)` + ` performs a default action for some Alt sequences.)`, ), 'audible-bell-sound': hterm.PreferenceManager.definePref_( 'Alert bell sound (URI)', - hterm.PreferenceManager.categories.Sounds, + hterm.PreferenceManager.Categories.Sounds, 'lib-resource:hterm/audio/bell', 'url', - `URL of the terminal bell sound. Empty string for no audible bell.` + `URL of the terminal bell sound. Leave it blank for no audible bell.`, ), 'desktop-notification-bell': hterm.PreferenceManager.definePref_( 'Create desktop notifications for alert bells', - hterm.PreferenceManager.categories.Sounds, + hterm.PreferenceManager.Categories.Sounds, false, 'bool', `If true, terminal bells in the background will create a Web ` + `Notification. https://www.w3.org/TR/notifications/\n` + `\n` + `Displaying notifications requires permission from the user. When this ` + `option is set to true, hterm will attempt to ask the user for ` + - `permission if necessary. Browsers may not show this permission ` + + `permission if necessary. Browsers might not show this permission ` + `request if it was not triggered by a user action.\n` + `\n` + `Chrome extensions with the "notifications" permission have permission ` + - `to display notifications.` + `to display notifications.`, ), 'background-color': hterm.PreferenceManager.definePref_( 'Background color', - hterm.PreferenceManager.categories.Appearance, + hterm.PreferenceManager.Categories.Appearance, 'rgb(16, 16, 16)', 'color', - `The background color for text with no other color attributes.` + `The background color for text with no other color attributes.`, ), 'background-image': hterm.PreferenceManager.definePref_( 'Background image', - hterm.PreferenceManager.categories.Appearance, + hterm.PreferenceManager.Categories.Appearance, '', 'string', - `CSS value of the background image. Empty string for no image.\n` + + `CSS value of the background image. Leave it blank for no image.\n` + `\n` + `For example:\n` + ` url(https://goo.gl/anedTK)\n` + - ` linear-gradient(top bottom, blue, red)` + ` linear-gradient(top bottom, blue, red)`, ), 'background-size': hterm.PreferenceManager.definePref_( 'Background image size', - hterm.PreferenceManager.categories.Appearance, + hterm.PreferenceManager.Categories.Appearance, '', 'string', - `CSS value of the background image size.` + `CSS value of the background image size.`, ), 'background-position': hterm.PreferenceManager.definePref_( 'Background image position', - hterm.PreferenceManager.categories.Appearance, + hterm.PreferenceManager.Categories.Appearance, '', 'string', `CSS value of the background image position.\n` + `\n` + `For example:\n` + ` 10% 10%\n` + - ` center` + ` center`, ), 'backspace-sends-backspace': hterm.PreferenceManager.definePref_( 'Backspace key behavior', - hterm.PreferenceManager.categories.Keyboard, + hterm.PreferenceManager.Categories.Keyboard, false, 'bool', - `If true, the backspace should send BS ('\\x08', aka ^H). Otherwise ` + - `the backspace key should send '\\x7f'.` + `If true, the Backspace key will send BS ('\\x08', aka ^H). Otherwise ` + + `the Backspace key will send '\\x7f'.`, ), 'character-map-overrides': hterm.PreferenceManager.definePref_( 'Character map overrides', - hterm.PreferenceManager.categories.Appearance, + hterm.PreferenceManager.Categories.Appearance, null, 'value', `This is specified as an object. It is a sparse array, where each ` + `property is the character set code and the value is an object that is ` + @@ -8501,53 +10212,57 @@ hterm.PreferenceManager.defaultPreferences = { `received character and the value is the displayed character.\n` + `\n` + `For example:\n` + - ` {"0":{"+":"\\u2192",",":"\\u2190","-":"\\u2191",".":"\\u2193", ` + - `"0":"\\u2588"}}` + `{ "0": {\n` + + ` "+": "\\u2192",\n` + + ` ",": "\\u2190",\n` + + ` "-": "\\u2191",\n` + + ` ".": "\\u2193",\n` + + ` "0": "\\u2588"\n} }`, ), 'close-on-exit': hterm.PreferenceManager.definePref_( 'Close window on exit', - hterm.PreferenceManager.categories.Miscellaneous, + hterm.PreferenceManager.Categories.Miscellaneous, true, 'bool', - `Whether to close the window when the command finishes executing.` + `Whether to close the window when the command finishes executing.`, ), 'cursor-blink': hterm.PreferenceManager.definePref_( 'Cursor blink', - hterm.PreferenceManager.categories.Appearance, + hterm.PreferenceManager.Categories.Appearance, false, 'bool', `Whether the text cursor blinks by default. This can be toggled at ` + - `runtime via terminal escape sequences.` + `runtime via terminal escape sequences.`, ), 'cursor-blink-cycle': hterm.PreferenceManager.definePref_( 'Cursor blink rate', - hterm.PreferenceManager.categories.Appearance, + hterm.PreferenceManager.Categories.Appearance, [1000, 500], 'value', `The text cursor blink rate in milliseconds.\n` + `\n` + `A two element array, the first of which is how long the text cursor ` + - `should be on, second is how long it should be off.` + `should be on, second is how long it should be off.`, ), 'cursor-shape': hterm.PreferenceManager.definePref_( 'Text cursor shape', - hterm.PreferenceManager.categories.Appearance, + hterm.PreferenceManager.Categories.Appearance, 'BLOCK', ['BLOCK', 'BEAM', 'UNDERLINE'], - `The shape of the visible text cursor. This can be toggled at ` + - `runtime via terminal escape sequences.` + `The shape of the visible text cursor. This can be changed at ` + + `runtime via terminal escape sequences.`, ), 'cursor-color': hterm.PreferenceManager.definePref_( 'Text cursor color', - hterm.PreferenceManager.categories.Appearance, + hterm.PreferenceManager.Categories.Appearance, 'rgba(255, 0, 0, 0.5)', 'color', - `The color of the visible text cursor.` + `The color of the visible text cursor.`, ), 'color-palette-overrides': hterm.PreferenceManager.definePref_( 'Initial color palette', - hterm.PreferenceManager.categories.Appearance, + hterm.PreferenceManager.Categories.Appearance, null, 'value', `Override colors in the default palette.\n` + `\n` + @@ -8565,19 +10280,19 @@ hterm.PreferenceManager.defaultPreferences = { `\n` + `For example, these both set color index 1 to blue:\n` + ` {1: "#0000ff"}\n` + - ` [null, "#0000ff"]` + ` [null, "#0000ff"]`, ), 'copy-on-select': hterm.PreferenceManager.definePref_( 'Automatically copy selected content', - hterm.PreferenceManager.categories.CopyPaste, + hterm.PreferenceManager.Categories.CopyPaste, true, 'bool', - `Automatically copy mouse selection to the clipboard.` + `Automatically copy mouse selection to the clipboard.`, ), 'use-default-window-copy': hterm.PreferenceManager.definePref_( 'Let the browser handle text copying', - hterm.PreferenceManager.categories.CopyPaste, + hterm.PreferenceManager.Categories.CopyPaste, false, 'bool', `Whether to use the default browser/OS's copy behavior.\n` + `\n` + @@ -8586,121 +10301,121 @@ hterm.PreferenceManager.defaultPreferences = { `at all), but makes the text selection less robust.\n` + `\n` + `For example, long lines that were automatically line wrapped will ` + - `be copied with the newlines still in them.` + `be copied with the newlines still in them.`, ), 'clear-selection-after-copy': hterm.PreferenceManager.definePref_( 'Automatically clear text selection', - hterm.PreferenceManager.categories.CopyPaste, + hterm.PreferenceManager.Categories.CopyPaste, true, 'bool', - `Whether to clear the selection after copying.` + `Whether to clear the selection after copying.`, ), 'ctrl-plus-minus-zero-zoom': hterm.PreferenceManager.definePref_( - 'Ctrl-+/-/0 zoom behavior', - hterm.PreferenceManager.categories.Keyboard, + 'Ctrl++/-/0 zoom behavior', + hterm.PreferenceManager.Categories.Keyboard, true, 'bool', - `If true, Ctrl-Plus/Minus/Zero controls zoom.\n` + - `If false, Ctrl-Shift-Plus/Minus/Zero controls zoom, Ctrl-Minus sends ` + - `^_, Ctrl-Plus/Zero do nothing.` + `If true, Ctrl+Plus/Minus/Zero controls zoom.\n` + + `If false, Ctrl+Shift+Plus/Minus/Zero controls zoom, Ctrl+Minus sends ` + + `^_, Ctrl+Plus/Zero do nothing.`, ), 'ctrl-c-copy': hterm.PreferenceManager.definePref_( - 'Ctrl-C copy behavior', - hterm.PreferenceManager.categories.Keyboard, + 'Ctrl+C copy behavior', + hterm.PreferenceManager.Categories.Keyboard, false, 'bool', - `Ctrl-C copies if true, send ^C to host if false.\n` + - `Ctrl-Shift-C sends ^C to host if true, copies if false.` + `Ctrl+C copies if true, send ^C to host if false.\n` + + `Ctrl+Shift+C sends ^C to host if true, copies if false.`, ), 'ctrl-v-paste': hterm.PreferenceManager.definePref_( - 'Ctrl-V paste behavior', - hterm.PreferenceManager.categories.Keyboard, + 'Ctrl+V paste behavior', + hterm.PreferenceManager.Categories.Keyboard, false, 'bool', - `Ctrl-V pastes if true, send ^V to host if false.\n` + - `Ctrl-Shift-V sends ^V to host if true, pastes if false.` + `Ctrl+V pastes if true, send ^V to host if false.\n` + + `Ctrl+Shift+V sends ^V to host if true, pastes if false.`, ), 'east-asian-ambiguous-as-two-column': hterm.PreferenceManager.definePref_( 'East Asian Ambiguous use two columns', - hterm.PreferenceManager.categories.Keyboard, + hterm.PreferenceManager.Categories.Keyboard, false, 'bool', - `Whether East Asian Ambiguous characters have two column width.` + `Whether East Asian Ambiguous characters have two column width.`, ), 'enable-8-bit-control': hterm.PreferenceManager.definePref_( 'Support non-UTF-8 C1 control characters', - hterm.PreferenceManager.categories.Keyboard, + hterm.PreferenceManager.Categories.Keyboard, false, 'bool', `True to enable 8-bit control characters, false to ignore them.\n` + `\n` + `We'll respect the two-byte versions of these control characters ` + - `regardless of this setting.` + `regardless of this setting.`, ), 'enable-bold': hterm.PreferenceManager.definePref_( 'Bold text behavior', - hterm.PreferenceManager.categories.Appearance, + hterm.PreferenceManager.Categories.Appearance, null, 'tristate', `If true, use bold weight font for text with the bold/bright ` + - `attribute. False to use the normal weight font. Null to autodetect.` + `attribute. False to use the normal weight font. Null to autodetect.`, ), 'enable-bold-as-bright': hterm.PreferenceManager.definePref_( 'Use bright colors with bold text', - hterm.PreferenceManager.categories.Appearance, + hterm.PreferenceManager.Categories.Appearance, true, 'bool', `If true, use bright colors (8-15 on a 16 color palette) for any text ` + - `with the bold attribute. False otherwise.` + `with the bold attribute. False otherwise.`, ), 'enable-blink': hterm.PreferenceManager.definePref_( 'Enable blinking text', - hterm.PreferenceManager.categories.Appearance, + hterm.PreferenceManager.Categories.Appearance, true, 'bool', - `If true, respect the blink attribute. False to ignore it.` + `If true, respect the blink attribute. False to ignore it.`, ), 'enable-clipboard-notice': hterm.PreferenceManager.definePref_( 'Show notification when copying content', - hterm.PreferenceManager.categories.CopyPaste, + hterm.PreferenceManager.Categories.CopyPaste, true, 'bool', `Whether to show a message in the terminal when the host writes to the ` + - `clipboard.` + `clipboard.`, ), 'enable-clipboard-write': hterm.PreferenceManager.definePref_( 'Allow remote clipboard writes', - hterm.PreferenceManager.categories.CopyPaste, + hterm.PreferenceManager.Categories.CopyPaste, true, 'bool', `Allow the remote host to write directly to the local system ` + `clipboard.\n` + `Read access is never granted regardless of this setting.\n` + `\n` + - `This is used to control access to features like OSC-52.` + `This is used to control access to features like OSC-52.`, ), 'enable-dec12': hterm.PreferenceManager.definePref_( 'Allow changing of text cursor blinking', - hterm.PreferenceManager.categories.Miscellaneous, + hterm.PreferenceManager.Categories.Miscellaneous, false, 'bool', `Respect the host's attempt to change the text cursor blink status ` + - `using DEC Private Mode 12.` + `using DEC Private Mode 12.`, ), 'enable-csi-j-3': hterm.PreferenceManager.definePref_( 'Allow clearing of scrollback buffer (CSI-J-3)', - hterm.PreferenceManager.categories.Miscellaneous, + hterm.PreferenceManager.Categories.Miscellaneous, true, 'bool', - `Whether CSI-J (Erase Display) mode 3 may clear the terminal ` + - `scrollback buffer.\n` + + `Whether the Erase Saved Lines function (mode 3) of the Erase Display ` + + `command (CSI-J) may clear the terminal scrollback buffer.\n` + `\n` + - `Enabling this by default is safe.` + `Enabling this by default is safe.`, ), 'environment': hterm.PreferenceManager.definePref_( 'Environment variables', - hterm.PreferenceManager.categories.Miscellaneous, + hterm.PreferenceManager.Categories.Miscellaneous, { // Signal ncurses based apps to use UTF-8 output instead of legacy // drawing modes (which only work in ISO-2022 mode). Since hterm is @@ -8713,62 +10428,83 @@ hterm.PreferenceManager.defaultPreferences = { 'COLORTERM': 'truecolor', }, 'value', - `The initial set of environment variables, as an object.` + `The initial set of environment variables, as an object.`, + ), + + 'find-result-color': hterm.PreferenceManager.definePref_( + 'Find results highlight color', + hterm.PreferenceManager.Categories.Appearance, + 'rgba(102, 204, 255, 0.4)', 'color', + `The background color to highlight find results.`, + ), + + 'find-result-selected-color': hterm.PreferenceManager.definePref_( + 'Find results selected highlight color', + hterm.PreferenceManager.Categories.Appearance, + 'rgba(102, 204, 255, 0.8)', 'color', + `The background color to highlight the selected find result.`, ), 'font-family': hterm.PreferenceManager.definePref_( 'Text font family', - hterm.PreferenceManager.categories.Appearance, + hterm.PreferenceManager.Categories.Appearance, '"DejaVu Sans Mono", "Noto Sans Mono", "Everson Mono", FreeMono, ' + 'Menlo, Terminal, monospace', 'string', - `Default font family for the terminal text.` + `Default font family for the terminal text.`, ), 'font-size': hterm.PreferenceManager.definePref_( 'Text font size', - hterm.PreferenceManager.categories.Appearance, + hterm.PreferenceManager.Categories.Appearance, 15, 'int', - `The default font size in pixels.` + `The default font size in pixels.`, ), 'font-smoothing': hterm.PreferenceManager.definePref_( 'Text font smoothing', - hterm.PreferenceManager.categories.Appearance, + hterm.PreferenceManager.Categories.Appearance, 'antialiased', 'string', - `CSS font-smoothing property.` + `CSS font-smoothing property.`, ), 'foreground-color': hterm.PreferenceManager.definePref_( 'Text color', - hterm.PreferenceManager.categories.Appearance, + hterm.PreferenceManager.Categories.Appearance, 'rgb(240, 240, 240)', 'color', - `The foreground color for text with no other color attributes.` + `The foreground color for text with no other color attributes.`, + ), + + 'enable-resize-status': hterm.PreferenceManager.definePref_( + 'Show terminal dimensions when resized', + hterm.PreferenceManager.Categories.Appearance, + false, 'bool', + `Whether to show terminal dimensions when the terminal changes size.`, ), 'hide-mouse-while-typing': hterm.PreferenceManager.definePref_( 'Hide mouse cursor while typing', - hterm.PreferenceManager.categories.Keyboard, + hterm.PreferenceManager.Categories.Keyboard, null, 'tristate', `Whether to automatically hide the mouse cursor when typing. ` + `By default, autodetect whether the platform/OS handles this.\n` + `\n` + - `Note: Some operating systems may override this setting and thus you ` + - `might not be able to always disable it.` + `Note: Your operating system might override this setting and thus you ` + + `might not be able to always disable it.`, ), 'home-keys-scroll': hterm.PreferenceManager.definePref_( 'Home/End key scroll behavior', - hterm.PreferenceManager.categories.Keyboard, + hterm.PreferenceManager.Categories.Keyboard, false, 'bool', - `If true, Home/End controls the terminal scrollbar and Shift-Home/` + - `Shift-End are sent to the remote host. If false, then Home/End are ` + - `sent to the remote host and Shift-Home/Shift-End scrolls.` + `If true, Home/End controls the terminal scrollbar and Shift+Home/` + + `Shift+End are sent to the remote host. If false, then Home/End are ` + + `sent to the remote host and Shift+Home/Shift+End scrolls.`, ), 'keybindings': hterm.PreferenceManager.definePref_( 'Keyboard bindings/shortcuts', - hterm.PreferenceManager.categories.Keyboard, + hterm.PreferenceManager.Categories.Keyboard, null, 'value', `A map of key sequence to key actions. Key sequences include zero or ` + `more modifier keys followed by a key code. Key codes can be decimal ` + @@ -8778,50 +10514,59 @@ hterm.PreferenceManager.defaultPreferences = { `\n` + `Sample keybindings:\n` + `{\n` + - ` "Ctrl-Alt-K": "clearTerminal",\n` + - ` "Ctrl-Shift-L": "PASS",\n` + - ` "Ctrl-H": "'Hello World'"\n` + - `}` + ` "Ctrl+Alt+K": "clearTerminal",\n` + + ` "Ctrl+Shift+L": "PASS",\n` + + ` "Ctrl+H": "'Hello World'"\n` + + `}`, + ), + + 'keybindings-os-defaults': hterm.PreferenceManager.definePref_( + 'Use default OS Keyboard bindings/shortcuts', + hterm.PreferenceManager.Categories.Keyboard, + false, 'bool', + `Whether common OS keyboard bindings should be respected instead of ` + + `always capturing for hterm's own use.`, ), 'media-keys-are-fkeys': hterm.PreferenceManager.definePref_( 'Media keys are Fkeys', - hterm.PreferenceManager.categories.Keyboard, + hterm.PreferenceManager.Categories.Keyboard, false, 'bool', `If true, convert media keys to their Fkey equivalent. If false, let ` + - `the browser handle the keys.` + `the browser handle the keys.`, ), 'meta-sends-escape': hterm.PreferenceManager.definePref_( 'Meta key modifier handling', - hterm.PreferenceManager.categories.Keyboard, + hterm.PreferenceManager.Categories.Keyboard, true, 'bool', `Send an ESC prefix when pressing a key while holding the Meta key.\n` + `\n` + - `For example, when enabled, pressing Meta-K will send ^[k as if you ` + - `typed Escape then k. When disabled, only k will be sent.` + `For example, when enabled, pressing Meta+K will send ^[k as if you ` + + `typed Escape then k. When disabled, only k will be sent.`, ), 'mouse-right-click-paste': hterm.PreferenceManager.definePref_( 'Mouse right clicks paste content', - hterm.PreferenceManager.categories.CopyPaste, + hterm.PreferenceManager.Categories.CopyPaste, true, 'bool', `Paste on right mouse button clicks.\n` + `\n` + `This option is independent of the "mouse-paste-button" setting.\n` + `\n` + - `Note: This will handle left & right handed mice correctly.` + `Note: The primary & secondary buttons are handled for you with left ` + + `& right handed mice.`, ), 'mouse-paste-button': hterm.PreferenceManager.definePref_( 'Mouse button paste', - hterm.PreferenceManager.categories.CopyPaste, + hterm.PreferenceManager.Categories.CopyPaste, null, [null, 0, 1, 2, 3, 4, 5, 6], - `Mouse paste button, or null to autodetect.\n` + + `The mouse button to use for pasting.\n` + `\n` + `For autodetect, we'll use the middle mouse button for non-X11 ` + `platforms (including Chrome OS). On X11, we'll use the right mouse ` + - `button (since the native window manager should paste via the middle ` + + `button (since the window manager should handle pasting via the middle ` + `mouse button).\n` + `\n` + `0 == left (primary) button.\n` + @@ -8830,172 +10575,239 @@ hterm.PreferenceManager.defaultPreferences = { `\n` + `This option is independent of the setting for right-click paste.\n` + `\n` + - `Note: This will handle left & right handed mice correctly.` + `Note: The primary & secondary buttons are handled for you with left ` + + `& right handed mice.`, + ), + + 'screen-padding-size': hterm.PreferenceManager.definePref_( + 'Screen padding size', + hterm.PreferenceManager.Categories.Appearance, + 8, 'int', + `The padding size in pixels around the border of the terminal screen.\n` + + `\n` + + `This controls the size of the border around the terminal screen so ` + + `the user can add some visible padding to the edges of the screen.`, + ), + + 'screen-border-size': hterm.PreferenceManager.definePref_( + 'Screen border size', + hterm.PreferenceManager.Categories.Appearance, + 0, 'int', + `The border size in pixels around the terminal screen.\n` + + `\n` + + `This controls the size of the border around the terminal screen to ` + + `create a visible line at the edges of the screen.`, + ), + + 'screen-border-color': hterm.PreferenceManager.definePref_( + 'Screen border color', + hterm.PreferenceManager.Categories.Appearance, + 'rgb(128, 128, 128)', 'color', + `The color for the border around the terminal screen.\n` + + `\n` + + `This controls the color of the border around the terminal screen to ` + + `create a visible line at the edges of the screen.`, ), 'word-break-match-left': hterm.PreferenceManager.definePref_( 'Automatic selection halting (to the left)', - hterm.PreferenceManager.categories.CopyPaste, - '[^\\s\\[\\](){}<>"\'\\^!@#$%&*,;:`]', 'string', + hterm.PreferenceManager.Categories.CopyPaste, + // TODO(vapier): Switch \u back to ‘“‹« once builders are fixed. + '[^\\s[\\](){}<>"\'^!@#$%&*,;:`\u{2018}\u{201c}\u{2039}\u{ab}]', 'string', `Regular expression to halt matching to the left (start) of a ` + `selection.\n` + `\n` + `Normally this is a character class to reject specific characters.\n` + - `We allow "~" and "." by default as paths frequently start with those.` + `We allow "~" and "." by default as paths frequently start with those.`, ), 'word-break-match-right': hterm.PreferenceManager.definePref_( 'Automatic selection halting (to the right)', - hterm.PreferenceManager.categories.CopyPaste, - '[^\\s\\[\\](){}<>"\'\\^!@#$%&*,;:~.`]', 'string', + hterm.PreferenceManager.Categories.CopyPaste, + // TODO(vapier): Switch \u back to ’”›» once builders are fixed. + '[^\\s[\\](){}<>"\'^!@#$%&*,;:~.`\u{2019}\u{201d}\u{203a}\u{bb}]', + 'string', `Regular expression to halt matching to the right (end) of a ` + `selection.\n` + `\n` + - `Normally this is a character class to reject specific characters.` + `Normally this is a character class to reject specific characters.`, ), 'word-break-match-middle': hterm.PreferenceManager.definePref_( 'Word break characters', - hterm.PreferenceManager.categories.CopyPaste, - '[^\\s\\[\\](){}<>"\'\\^]*', 'string', + hterm.PreferenceManager.Categories.CopyPaste, + '[^\\s[\\](){}<>"\'^]*', 'string', `Regular expression to match all the characters in the middle.\n` + `\n` + `Normally this is a character class to reject specific characters.\n` + `\n` + - `Used to expand the selection surrounding the starting point.` + `Used to expand the selection surrounding the starting point.`, ), 'page-keys-scroll': hterm.PreferenceManager.definePref_( 'Page Up/Down key scroll behavior', - hterm.PreferenceManager.categories.Keyboard, + hterm.PreferenceManager.Categories.Keyboard, false, 'bool', `If true, Page Up/Page Down controls the terminal scrollbar and ` + - `Shift-Page Up/Shift-Page Down are sent to the remote host. If false, ` + - `then Page Up/Page Down are sent to the remote host and Shift-Page Up/` + - `Shift-Page Down scrolls.` + `Shift+Page Up/Shift+Page Down are sent to the remote host. If false, ` + + `then Page Up/Page Down are sent to the remote host and Shift+Page Up/` + + `Shift+Page Down scrolls.`, ), 'pass-alt-number': hterm.PreferenceManager.definePref_( - 'Pass Alt-1..9 key behavior', - hterm.PreferenceManager.categories.Keyboard, + 'Alt+1..9 switch tab/app behavior', + hterm.PreferenceManager.Categories.Keyboard, null, 'tristate', - `Whether Alt-1..9 is passed to the browser.\n` + + `Whether Alt+1..9 is passed to the browser.\n` + `\n` + `This is handy when running hterm in a browser tab, so that you don't ` + - `lose Chrome's "switch to tab" keyboard accelerators. When not running ` + - `in a tab it's better to send these keys to the host so they can be ` + - `used in vim or emacs.\n` + + `lose Chrome's "switch to tab/app" keyboard shortcuts. When not ` + + `running in a tab it's better to send these keys to the host so they ` + + `can be used in vim or emacs.\n` + `\n` + - `If true, Alt-1..9 will be handled by the browser. If false, Alt-1..9 ` + + `If true, Alt+1..9 will be handled by the browser. If false, Alt+1..9 ` + `will be sent to the host. If null, autodetect based on browser ` + - `platform and window type.` + `platform and window type.`, ), 'pass-ctrl-number': hterm.PreferenceManager.definePref_( - 'Pass Ctrl-1..9 key behavior', - hterm.PreferenceManager.categories.Keyboard, + 'Ctrl+1..9 switch tab behavior', + hterm.PreferenceManager.Categories.Keyboard, null, 'tristate', - `Whether Ctrl-1..9 is passed to the browser.\n` + + `Whether Ctrl+1..9 is passed to the browser.\n` + `\n` + `This is handy when running hterm in a browser tab, so that you don't ` + - `lose Chrome's "switch to tab" keyboard accelerators. When not running ` + + `lose Chrome's "switch to tab" keyboard shortcuts. When not running ` + `in a tab it's better to send these keys to the host so they can be ` + `used in vim or emacs.\n` + `\n` + - `If true, Ctrl-1..9 will be handled by the browser. If false, ` + - `Ctrl-1..9 will be sent to the host. If null, autodetect based on ` + - `browser platform and window type.` + `If true, Ctrl+1..9 will be handled by the browser. If false, ` + + `Ctrl+1..9 will be sent to the host. If null, autodetect based on ` + + `browser platform and window type.`, + ), + + 'pass-ctrl-n': hterm.PreferenceManager.definePref_( + 'Ctrl+N new window behavior', + hterm.PreferenceManager.Categories.Keyboard, + false, 'bool', + `Whether Ctrl+N is passed to the browser.\n` + + `\n` + + `If true, Ctrl+N will be handled by the browser as the "new window" ` + + `keyboard shortcut. If false, Ctrl+N will be sent to the host.`, + ), + + 'pass-ctrl-t': hterm.PreferenceManager.definePref_( + 'Ctrl+T new tab behavior', + hterm.PreferenceManager.Categories.Keyboard, + false, 'bool', + `Whether Ctrl+T is passed to the browser.\n` + + `\n` + + `If true, Ctrl+T will be handled by the browser as the "new tab" ` + + `keyboard shortcut. If false, Ctrl+T will be sent to the host.`, + ), + + 'pass-ctrl-tab': hterm.PreferenceManager.definePref_( + 'Ctrl+Tab switch tab behavior', + hterm.PreferenceManager.Categories.Keyboard, + false, 'bool', + `Whether Ctrl+Tab and Ctrl+Shift+Tab are passed to the browser.\n` + + `\n` + + `If true, Ctrl+Tab and Ctrl+Shift+Tab will be handled by the browser ` + + `as the "next/previous tab" keyboard shortcut. If false, the Tab ` + + `key is sent to the host without Ctrl or Shift.`, + ), + + 'pass-ctrl-w': hterm.PreferenceManager.definePref_( + 'Ctrl+W close tab behavior', + hterm.PreferenceManager.Categories.Keyboard, + false, 'bool', + `Whether Ctrl+W is passed to the browser.\n` + + `\n` + + `If true, Ctrl+W will be handled by the browser as the "close tab" ` + + `keyboard shortcut. If false, Ctrl+W will be sent to the host.`, ), 'pass-meta-number': hterm.PreferenceManager.definePref_( - 'Pass Meta-1..9 key behavior', - hterm.PreferenceManager.categories.Keyboard, + 'Meta+1..9 switch tab behavior', + hterm.PreferenceManager.Categories.Keyboard, null, 'tristate', - `Whether Meta-1..9 is passed to the browser.\n` + + `Whether Meta+1..9 is passed to the browser.\n` + `\n` + `This is handy when running hterm in a browser tab, so that you don't ` + - `lose Chrome's "switch to tab" keyboard accelerators. When not running ` + + `lose Chrome's "switch to tab" keyboard shortcuts. When not running ` + `in a tab it's better to send these keys to the host so they can be ` + `used in vim or emacs.\n` + `\n` + - `If true, Meta-1..9 will be handled by the browser. If false, ` + - `Meta-1..9 will be sent to the host. If null, autodetect based on ` + - `browser platform and window type.` + `If true, Meta+1..9 will be handled by the browser. If false, ` + + `Meta+1..9 will be sent to the host. If null, autodetect based on ` + + `browser platform and window type.`, ), 'pass-meta-v': hterm.PreferenceManager.definePref_( - 'Pass Meta-V key behavior', - hterm.PreferenceManager.categories.Keyboard, + 'Meta+V paste behavior', + hterm.PreferenceManager.Categories.Keyboard, true, 'bool', - `Whether Meta-V gets passed to host.` + `Whether Meta+V gets passed to the browser.\n` + + `\n` + + `On some systems, this is used to paste content.`, ), 'paste-on-drop': hterm.PreferenceManager.definePref_( 'Allow drag & drop to paste', - hterm.PreferenceManager.categories.CopyPaste, + hterm.PreferenceManager.Categories.CopyPaste, true, 'bool', `If true, Drag and dropped text will paste into terminal.\n` + - `If false, dropped text will be ignored.` - ), - - 'receive-encoding': hterm.PreferenceManager.definePref_( - 'Receive encoding', - hterm.PreferenceManager.categories.Encoding, - 'utf-8', ['utf-8', 'raw'], - `Set the expected encoding for data received from the host.\n` + - `If the encodings do not match, visual bugs are likely to be ` + - `observed.\n` + - `\n` + - `Valid values are 'utf-8' and 'raw'.` + `If false, dropped text will be ignored.`, ), 'scroll-on-keystroke': hterm.PreferenceManager.definePref_( 'Scroll to bottom after keystroke', - hterm.PreferenceManager.categories.Scrolling, + hterm.PreferenceManager.Categories.Scrolling, true, 'bool', - `Whether to scroll to the bottom on any keystroke.` + `Whether to scroll to the bottom on any keystroke.`, ), 'scroll-on-output': hterm.PreferenceManager.definePref_( 'Scroll to bottom after new output', - hterm.PreferenceManager.categories.Scrolling, + hterm.PreferenceManager.Categories.Scrolling, false, 'bool', - `Whether to scroll to the bottom on terminal output.` + `Whether to scroll to the bottom on terminal output.`, ), 'scrollbar-visible': hterm.PreferenceManager.definePref_( 'Scrollbar visibility', - hterm.PreferenceManager.categories.Scrolling, + hterm.PreferenceManager.Categories.Scrolling, true, 'bool', - `The vertical scrollbar mode.` + `The vertical scrollbar mode.`, ), 'scroll-wheel-may-send-arrow-keys': hterm.PreferenceManager.definePref_( 'Emulate arrow keys with scroll wheel', - hterm.PreferenceManager.categories.Scrolling, + hterm.PreferenceManager.Categories.Scrolling, false, 'bool', `When using the alternative screen buffer, and DECCKM (Application ` + - `Cursor Keys) is active, mouse wheel scroll events will emulate arrow ` + + `Cursor Keys) is active, mouse scroll wheel events will emulate arrow ` + `keys.\n` + `\n` + `It can be temporarily disabled by holding the Shift key.\n` + `\n` + `This frequently comes up when using pagers (less) or reading man ` + - `pages or text editors (vi/nano) or using screen/tmux.` + `pages or text editors (vi/nano) or using screen/tmux.`, ), 'scroll-wheel-move-multiplier': hterm.PreferenceManager.definePref_( 'Mouse scroll wheel multiplier', - hterm.PreferenceManager.categories.Scrolling, + hterm.PreferenceManager.Categories.Scrolling, 1, 'int', - `The multiplier for scroll wheel events when measured in pixels.\n` + + `The multiplier for mouse scroll wheel events when measured in ` + + `pixels.\n` + `\n` + - `Alters how fast the page scrolls.` + `Alters how fast the page scrolls.`, ), 'terminal-encoding': hterm.PreferenceManager.definePref_( 'Terminal encoding', - hterm.PreferenceManager.categories.Encoding, + hterm.PreferenceManager.Categories.Encoding, 'utf-8', ['iso-2022', 'utf-8', 'utf-8-locked'], `The default terminal encoding (DOCS).\n` + `\n` + @@ -9006,42 +10818,43 @@ hterm.PreferenceManager.defaultPreferences = { `via terminal escape sequences.\n` + `\n` + `You should stick with UTF-8 unless you notice broken rendering with ` + - `legacy applications.` + `legacy applications.`, ), 'shift-insert-paste': hterm.PreferenceManager.definePref_( - 'Shift-Insert paste', - hterm.PreferenceManager.categories.Keyboard, + 'Shift+Insert paste', + hterm.PreferenceManager.Categories.Keyboard, true, 'bool', - `Whether Shift-Insert is used for pasting or is sent to the remote host.` + `Whether Shift+Insert is used for pasting or is sent to the remote host.`, ), 'user-css': hterm.PreferenceManager.definePref_( 'Custom CSS (URI)', - hterm.PreferenceManager.categories.Appearance, + hterm.PreferenceManager.Categories.Appearance, '', 'url', - `URL of user stylesheet to include in the terminal document.` + `URL of user stylesheet to include in the terminal document.`, ), 'user-css-text': hterm.PreferenceManager.definePref_( 'Custom CSS (inline text)', - hterm.PreferenceManager.categories.Appearance, + hterm.PreferenceManager.Categories.Appearance, '', 'multiline-string', - `Custom CSS text for styling the terminal.` + `Custom CSS text for styling the terminal.`, ), 'allow-images-inline': hterm.PreferenceManager.definePref_( 'Allow inline image display', - hterm.PreferenceManager.categories.Extensions, + hterm.PreferenceManager.Categories.Extensions, null, 'tristate', `Whether to allow the remote host to display images in the terminal.\n` + `\n` + - `By default, we prompt until a choice is made.` + `By default, we prompt until a choice is made.`, ), }; hterm.PreferenceManager.prototype = Object.create(lib.PreferenceManager.prototype); +/** @override */ hterm.PreferenceManager.constructor = hterm.PreferenceManager; // SOURCE FILE: hterm/js/hterm_pubsub.js // Copyright (c) 2012 The Chromium OS Authors. All rights reserved. @@ -9051,6 +10864,8 @@ hterm.PreferenceManager.constructor = hterm.PreferenceManager; /** * Utility class used to add publish/subscribe/unsubscribe functionality to * an existing object. + * + * @constructor */ hterm.PubSub = function() { this.observers_ = {}; @@ -9062,11 +10877,11 @@ hterm.PubSub = function() { * No other properties of the object are touched, so there is no need to * worry about clashing private properties. * - * @param {Object} obj The object to add this behavior to. + * @param {!Object} obj The object to add this behavior to. */ hterm.PubSub.addBehavior = function(obj) { - var pubsub = new hterm.PubSub(); - for (var m in hterm.PubSub.prototype) { + const pubsub = new hterm.PubSub(); + for (const m in hterm.PubSub.prototype) { obj[m] = hterm.PubSub.prototype[m].bind(pubsub); } }; @@ -9075,11 +10890,12 @@ hterm.PubSub.addBehavior = function(obj) { * Subscribe to be notified of messages about a subject. * * @param {string} subject The subject to subscribe to. - * @param {function(Object)} callback The function to invoke for notifications. + * @param {function(...)} callback The function to invoke for notifications. */ hterm.PubSub.prototype.subscribe = function(subject, callback) { - if (!(subject in this.observers_)) + if (!(subject in this.observers_)) { this.observers_[subject] = []; + } this.observers_[subject].push(callback); }; @@ -9088,17 +10904,19 @@ hterm.PubSub.prototype.subscribe = function(subject, callback) { * Unsubscribe from a subject. * * @param {string} subject The subject to unsubscribe from. - * @param {function(Object)} callback A callback previously registered via + * @param {function(...)} callback A callback previously registered via * subscribe(). */ hterm.PubSub.prototype.unsubscribe = function(subject, callback) { - var list = this.observers_[subject]; - if (!list) - throw 'Invalid subject: ' + subject; + const list = this.observers_[subject]; + if (!list) { + throw new Error(`Invalid subject: ${subject}`); + } - var i = list.indexOf(callback); - if (i < 0) - throw 'Not subscribed: ' + subject; + const i = list.indexOf(callback); + if (i < 0) { + throw new Error(`Not subscribed: ${subject}`); + } list.splice(i, 1); }; @@ -9110,36 +10928,39 @@ hterm.PubSub.prototype.unsubscribe = function(subject, callback) { * This method will return before anyone is actually notified. * * @param {string} subject The subject to publish about. - * @param {Object} e An arbitrary object associated with this notification. - * @param {function(Object)} opt_lastCallback An optional function to call after - * all subscribers have been notified. + * @param {?Object=} e An arbitrary object associated with this notification. + * @param {function(!Object)=} lastCallback An optional function to call + * after all subscribers have been notified. */ -hterm.PubSub.prototype.publish = function(subject, e, opt_lastCallback) { +hterm.PubSub.prototype.publish = function( + subject, e, lastCallback = undefined) { function notifyList(i) { // Set this timeout before invoking the callback, so we don't have to // concern ourselves with exceptions. - if (i < list.length - 1) + if (i < list.length - 1) { setTimeout(notifyList, 0, i + 1); + } list[i](e); } - var list = this.observers_[subject]; + let list = this.observers_[subject]; if (list) { // Copy the list, in case it changes while we're notifying. list = [].concat(list); } - if (opt_lastCallback) { + if (lastCallback) { if (list) { - list.push(opt_lastCallback); + list.push(lastCallback); } else { - list = [opt_lastCallback]; + list = [lastCallback]; } } - if (list) + if (list) { setTimeout(notifyList, 0, 0); + } }; // SOURCE FILE: hterm/js/hterm_screen.js // Copyright (c) 2012 The Chromium OS Authors. All rights reserved. @@ -9166,7 +10987,7 @@ hterm.PubSub.prototype.publish = function(subject, e, opt_lastCallback) { * - The hterm.Terminal class holds two hterm.Screen instances. One for the * primary screen and one for the alternate screen. * - * - The html.Screen class only cares that rows are HTMLElements. In the + * - The html.Screen class only cares that rows are HTML Elements. In the * larger context of hterm, however, the rows happen to be displayed by an * hterm.ScrollPort and have to follow a few rules as a result. Each * row must be rooted by the custom HTML tag 'x-row', and each must have a @@ -9181,19 +11002,22 @@ hterm.PubSub.prototype.publish = function(subject, e, opt_lastCallback) { * * The screen initially has no rows and a maximum column count of 0. * - * @param {integer} opt_columnCount The maximum number of columns for this + * @param {number=} columnCount The maximum number of columns for this * screen. See insertString() and overwriteString() for information about * what happens when too many characters are added too a row. Defaults to * 0 if not provided. + * @constructor */ -hterm.Screen = function(opt_columnCount) { +hterm.Screen = function(columnCount = 0) { /** * Public, read-only access to the rows in this screen. + * + * @type {!Array} */ this.rowsArray = []; // The max column width for this screen. - this.columnCount_ = opt_columnCount || 80; + this.columnCount_ = columnCount; // The current color, bold, underline and blink attributes. this.textAttributes = new hterm.TextAttributes(window.document); @@ -9212,18 +11036,21 @@ hterm.Screen = function(opt_columnCount) { this.cursorNode_ = null; // The offset in column width into cursorNode_ where the cursor is positioned. - this.cursorOffset_ = null; + this.cursorOffset_ = 0; // Regexes for expanding word selections. + /** @type {?string} */ this.wordBreakMatchLeft = null; + /** @type {?string} */ this.wordBreakMatchRight = null; + /** @type {?string} */ this.wordBreakMatchMiddle = null; }; /** * Return the screen size as an hterm.Size object. * - * @return {hterm.Size} hterm.Size object representing the current number + * @return {!hterm.Size} hterm.Size object representing the current number * of rows and columns in this screen. */ hterm.Screen.prototype.getSize = function() { @@ -9233,7 +11060,7 @@ hterm.Screen.prototype.getSize = function() { /** * Return the current number of rows in this screen. * - * @return {integer} The number of rows in this screen. + * @return {number} The number of rows in this screen. */ hterm.Screen.prototype.getHeight = function() { return this.rowsArray.length; @@ -9242,7 +11069,7 @@ hterm.Screen.prototype.getHeight = function() { /** * Return the current number of columns in this screen. * - * @return {integer} The number of columns in this screen. + * @return {number} The number of columns in this screen. */ hterm.Screen.prototype.getWidth = function() { return this.columnCount_; @@ -9251,19 +11078,20 @@ hterm.Screen.prototype.getWidth = function() { /** * Set the maximum number of columns per row. * - * @param {integer} count The maximum number of columns per row. + * @param {number} count The maximum number of columns per row. */ hterm.Screen.prototype.setColumnCount = function(count) { this.columnCount_ = count; - if (this.cursorPosition.column >= count) + if (this.cursorPosition.column >= count) { this.setCursorPosition(this.cursorPosition.row, count - 1); + } }; /** * Remove the first row from the screen and return it. * - * @return {HTMLElement} The first row in this screen. + * @return {!Element} The first row in this screen. */ hterm.Screen.prototype.shiftRow = function() { return this.shiftRows(1)[0]; @@ -9272,8 +11100,8 @@ hterm.Screen.prototype.shiftRow = function() { /** * Remove rows from the top of the screen and return them as an array. * - * @param {integer} count The number of rows to remove. - * @return {Array.} The selected rows. + * @param {number} count The number of rows to remove. + * @return {!Array} The selected rows. */ hterm.Screen.prototype.shiftRows = function(count) { return this.rowsArray.splice(0, count); @@ -9282,7 +11110,7 @@ hterm.Screen.prototype.shiftRows = function(count) { /** * Insert a row at the top of the screen. * - * @param {HTMLElement} row The row to insert. + * @param {!Element} row The row to insert. */ hterm.Screen.prototype.unshiftRow = function(row) { this.rowsArray.splice(0, 0, row); @@ -9291,7 +11119,7 @@ hterm.Screen.prototype.unshiftRow = function(row) { /** * Insert rows at the top of the screen. * - * @param {Array.} rows The rows to insert. + * @param {!Array} rows The rows to insert. */ hterm.Screen.prototype.unshiftRows = function(rows) { this.rowsArray.unshift.apply(this.rowsArray, rows); @@ -9300,7 +11128,7 @@ hterm.Screen.prototype.unshiftRows = function(rows) { /** * Remove the last row from the screen and return it. * - * @return {HTMLElement} The last row in this screen. + * @return {!Element} The last row in this screen. */ hterm.Screen.prototype.popRow = function() { return this.popRows(1)[0]; @@ -9309,8 +11137,8 @@ hterm.Screen.prototype.popRow = function() { /** * Remove rows from the bottom of the screen and return them as an array. * - * @param {integer} count The number of rows to remove. - * @return {Array.} The selected rows. + * @param {number} count The number of rows to remove. + * @return {!Array} The selected rows. */ hterm.Screen.prototype.popRows = function(count) { return this.rowsArray.splice(this.rowsArray.length - count, count); @@ -9319,7 +11147,7 @@ hterm.Screen.prototype.popRows = function(count) { /** * Insert a row at the bottom of the screen. * - * @param {HTMLElement} row The row to insert. + * @param {!Element} row The row to insert. */ hterm.Screen.prototype.pushRow = function(row) { this.rowsArray.push(row); @@ -9328,7 +11156,7 @@ hterm.Screen.prototype.pushRow = function(row) { /** * Insert rows at the bottom of the screen. * - * @param {Array.} rows The rows to insert. + * @param {!Array} rows The rows to insert. */ hterm.Screen.prototype.pushRows = function(rows) { rows.push.apply(this.rowsArray, rows); @@ -9337,8 +11165,8 @@ hterm.Screen.prototype.pushRows = function(rows) { /** * Insert a row at the specified row of the screen. * - * @param {integer} index The index to insert the row. - * @param {HTMLElement} row The row to insert. + * @param {number} index The index to insert the row. + * @param {!Element} row The row to insert. */ hterm.Screen.prototype.insertRow = function(index, row) { this.rowsArray.splice(index, 0, row); @@ -9347,11 +11175,11 @@ hterm.Screen.prototype.insertRow = function(index, row) { /** * Insert rows at the specified row of the screen. * - * @param {integer} index The index to insert the rows. - * @param {Array.} rows The rows to insert. + * @param {number} index The index to insert the rows. + * @param {!Array} rows The rows to insert. */ hterm.Screen.prototype.insertRows = function(index, rows) { - for (var i = 0; i < rows.length; i++) { + for (let i = 0; i < rows.length; i++) { this.rowsArray.splice(index + i, 0, rows[i]); } }; @@ -9359,8 +11187,8 @@ hterm.Screen.prototype.insertRows = function(index, rows) { /** * Remove a row from the screen and return it. * - * @param {integer} index The index of the row to remove. - * @return {HTMLElement} The selected row. + * @param {number} index The index of the row to remove. + * @return {!Element} The selected row. */ hterm.Screen.prototype.removeRow = function(index) { return this.rowsArray.splice(index, 1)[0]; @@ -9369,9 +11197,9 @@ hterm.Screen.prototype.removeRow = function(index) { /** * Remove rows from the bottom of the screen and return them as an array. * - * @param {integer} index The index to start removing rows. - * @param {integer} count The number of rows to remove. - * @return {Array.} The selected rows. + * @param {number} index The index to start removing rows. + * @param {number} count The number of rows to remove. + * @return {!Array} The selected rows. */ hterm.Screen.prototype.removeRows = function(index, count) { return this.rowsArray.splice(index, count); @@ -9390,7 +11218,7 @@ hterm.Screen.prototype.invalidateCursorPosition = function() { this.cursorPosition.move(0, 0); this.cursorRowNode_ = null; this.cursorNode_ = null; - this.cursorOffset_ = null; + this.cursorOffset_ = 0; }; /** @@ -9403,20 +11231,20 @@ hterm.Screen.prototype.clearCursorRow = function() { this.cursorPosition.column = 0; this.cursorPosition.overflow = false; - var text; + let text; if (this.textAttributes.isDefault()) { text = ''; } else { - text = lib.f.getWhitespace(this.columnCount_); + text = ' '.repeat(this.columnCount_); } // We shouldn't honor inverse colors when clearing an area, to match // xterm's back color erase behavior. - var inverse = this.textAttributes.inverse; + const inverse = this.textAttributes.inverse; this.textAttributes.inverse = false; this.textAttributes.syncColors(); - var node = this.textAttributes.createContainer(text); + const node = this.textAttributes.createContainer(text); this.cursorRowNode_.appendChild(node); this.cursorNode_ = node; @@ -9443,8 +11271,8 @@ hterm.Screen.prototype.commitLineOverflow = function() { /** * Relocate the cursor to a give row and column. * - * @param {integer} row The zero based row. - * @param {integer} column The zero based column. + * @param {number} row The zero based row. + * @param {number} column The zero based column. */ hterm.Screen.prototype.setCursorPosition = function(row, column) { if (!this.rowsArray.length) { @@ -9470,15 +11298,15 @@ hterm.Screen.prototype.setCursorPosition = function(row, column) { this.cursorPosition.overflow = false; - var rowNode = this.rowsArray[row]; - var node = rowNode.firstChild; + const rowNode = this.rowsArray[row]; + let node = rowNode.firstChild; if (!node) { node = rowNode.ownerDocument.createTextNode(''); rowNode.appendChild(node); } - var currentColumn = 0; + let currentColumn = 0; if (rowNode == this.cursorRowNode_) { if (column >= this.cursorPosition.column - this.cursorOffset_) { @@ -9492,8 +11320,8 @@ hterm.Screen.prototype.setCursorPosition = function(row, column) { this.cursorPosition.move(row, column); while (node) { - var offset = column - currentColumn; - var width = hterm.TextAttributes.nodeWidth(node); + const offset = column - currentColumn; + const width = hterm.TextAttributes.nodeWidth(node); if (!node.nextSibling || width > offset) { this.cursorNode_ = node; this.cursorOffset_ = offset; @@ -9508,6 +11336,8 @@ hterm.Screen.prototype.setCursorPosition = function(row, column) { /** * Set the provided selection object to be a caret selection at the current * cursor position. + * + * @param {!Selection} selection */ hterm.Screen.prototype.syncSelectionCaret = function(selection) { try { @@ -9530,28 +11360,30 @@ hterm.Screen.prototype.syncSelectionCaret = function(selection) { * The to-be-split node must have a container, so that the new node can be * placed next to it. * - * @param {HTMLNode} node The node to split. - * @param {integer} offset The offset into the node where the split should + * @param {!Node} node The node to split. + * @param {number} offset The offset into the node where the split should * occur. */ hterm.Screen.prototype.splitNode_ = function(node, offset) { - var afterNode = node.cloneNode(false); + const afterNode = node.cloneNode(false); - var textContent = node.textContent; + const textContent = node.textContent; node.textContent = hterm.TextAttributes.nodeSubstr(node, 0, offset); afterNode.textContent = lib.wc.substr(textContent, offset); - if (afterNode.textContent) + if (afterNode.textContent) { node.parentNode.insertBefore(afterNode, node.nextSibling); - if (!node.textContent) - node.parentNode.removeChild(node); + } + if (!node.textContent) { + node.remove(); + } }; /** * Ensure that text is clipped and the cursor is clamped to the column count. */ hterm.Screen.prototype.maybeClipCurrentRow = function() { - var width = hterm.TextAttributes.nodeWidth(this.cursorRowNode_); + let width = hterm.TextAttributes.nodeWidth(lib.notNull(this.cursorRowNode_)); if (width <= this.columnCount_) { // Current row does not need clipping, but may need clamping. @@ -9564,13 +11396,13 @@ hterm.Screen.prototype.maybeClipCurrentRow = function() { } // Save off the current column so we can maybe restore it later. - var currentColumn = this.cursorPosition.column; + const currentColumn = this.cursorPosition.column; // Move the cursor to the final column. this.setCursorPosition(this.cursorPosition.row, this.columnCount_ - 1); // Remove any text that partially overflows. - width = hterm.TextAttributes.nodeWidth(this.cursorNode_); + width = hterm.TextAttributes.nodeWidth(lib.notNull(this.cursorNode_)); if (this.cursorOffset_ < width - 1) { this.cursorNode_.textContent = hterm.TextAttributes.nodeSubstr( @@ -9578,8 +11410,8 @@ hterm.Screen.prototype.maybeClipCurrentRow = function() { } // Remove all nodes after the cursor. - var rowNode = this.cursorRowNode_; - var node = this.cursorNode_.nextSibling; + const rowNode = this.cursorRowNode_; + let node = this.cursorNode_.nextSibling; while (node) { rowNode.removeChild(node); @@ -9605,34 +11437,40 @@ hterm.Screen.prototype.maybeClipCurrentRow = function() { * * It is also up to the caller to properly maintain the line overflow state * using hterm.Screen..commitLineOverflow(). + * + * @param {string} str The string to insert. + * @param {number=} wcwidth The cached lib.wc.strWidth value for |str|. Will be + * calculated on demand if need be. Passing in a cached value helps speed + * up processing as this is a hot codepath. */ -hterm.Screen.prototype.insertString = function(str, wcwidth=undefined) { - var cursorNode = this.cursorNode_; - var cursorNodeText = cursorNode.textContent; +hterm.Screen.prototype.insertString = function(str, wcwidth = undefined) { + let cursorNode = this.cursorNode_; + let cursorNodeText = cursorNode.textContent; this.cursorRowNode_.removeAttribute('line-overflow'); // We may alter the width of the string by prepending some missing // whitespaces, so we need to record the string width ahead of time. - if (wcwidth === undefined) + if (wcwidth === undefined) { wcwidth = lib.wc.strWidth(str); + } // No matter what, before this function exits the cursor column will have // moved this much. this.cursorPosition.column += wcwidth; // Local cache of the cursor offset. - var offset = this.cursorOffset_; + let offset = this.cursorOffset_; // Reverse offset is the offset measured from the end of the string. // Zero implies that the cursor is at the end of the cursor node. - var reverseOffset = hterm.TextAttributes.nodeWidth(cursorNode) - offset; + let reverseOffset = hterm.TextAttributes.nodeWidth(cursorNode) - offset; if (reverseOffset < 0) { // A negative reverse offset means the cursor is positioned past the end // of the characters on this line. We'll need to insert the missing // whitespace. - var ws = lib.f.getWhitespace(-reverseOffset); + const ws = ' '.repeat(-reverseOffset); // This whitespace should be completely unstyled. Underline, background // color, and strikethrough would be visible on whitespace, so we can't use @@ -9658,7 +11496,7 @@ hterm.Screen.prototype.insertString = function(str, wcwidth=undefined) { cursorNode.textContent = (cursorNodeText += ws); } else { // Worst case, we have to create a new node to hold the whitespace. - var wsNode = cursorNode.ownerDocument.createTextNode(ws); + const wsNode = cursorNode.ownerDocument.createTextNode(ws); this.cursorRowNode_.insertBefore(wsNode, cursorNode.nextSibling); this.cursorNode_ = cursorNode = wsNode; this.cursorOffset_ = offset = -reverseOffset; @@ -9691,7 +11529,7 @@ hterm.Screen.prototype.insertString = function(str, wcwidth=undefined) { if (offset == 0) { // At the beginning of the cursor node, the check the previous sibling. - var previousSibling = cursorNode.previousSibling; + const previousSibling = cursorNode.previousSibling; if (previousSibling && this.textAttributes.matchesContainer(previousSibling)) { previousSibling.textContent += str; @@ -9700,7 +11538,7 @@ hterm.Screen.prototype.insertString = function(str, wcwidth=undefined) { return; } - var newNode = this.textAttributes.createContainer(str); + const newNode = this.textAttributes.createContainer(str); this.cursorRowNode_.insertBefore(newNode, cursorNode); this.cursorNode_ = newNode; this.cursorOffset_ = wcwidth; @@ -9709,7 +11547,7 @@ hterm.Screen.prototype.insertString = function(str, wcwidth=undefined) { if (reverseOffset == 0) { // At the end of the cursor node, the check the next sibling. - var nextSibling = cursorNode.nextSibling; + const nextSibling = cursorNode.nextSibling; if (nextSibling && this.textAttributes.matchesContainer(nextSibling)) { nextSibling.textContent = str + nextSibling.textContent; @@ -9718,7 +11556,7 @@ hterm.Screen.prototype.insertString = function(str, wcwidth=undefined) { return; } - var newNode = this.textAttributes.createContainer(str); + const newNode = this.textAttributes.createContainer(str); this.cursorRowNode_.insertBefore(newNode, nextSibling); this.cursorNode_ = newNode; // We specifically need to include any missing whitespace here, since it's @@ -9730,7 +11568,7 @@ hterm.Screen.prototype.insertString = function(str, wcwidth=undefined) { // Worst case, we're somewhere in the middle of the cursor node. We'll // have to split it into two nodes and insert our new container in between. this.splitNode_(cursorNode, offset); - var newNode = this.textAttributes.createContainer(str); + const newNode = this.textAttributes.createContainer(str); this.cursorRowNode_.insertBefore(newNode, cursorNode.nextSibling); this.cursorNode_ = newNode; this.cursorOffset_ = wcwidth; @@ -9744,17 +11582,25 @@ hterm.Screen.prototype.insertString = function(str, wcwidth=undefined) { * * It is also up to the caller to properly maintain the line overflow state * using hterm.Screen..commitLineOverflow(). + * + * @param {string} str The source string for overwriting existing content. + * @param {number=} wcwidth The cached lib.wc.strWidth value for |str|. Will be + * calculated on demand if need be. Passing in a cached value helps speed + * up processing as this is a hot codepath. */ -hterm.Screen.prototype.overwriteString = function(str, wcwidth=undefined) { - var maxLength = this.columnCount_ - this.cursorPosition.column; - if (!maxLength) +hterm.Screen.prototype.overwriteString = function(str, wcwidth = undefined) { + const maxLength = this.columnCount_ - this.cursorPosition.column; + if (!maxLength) { return; + } - if (wcwidth === undefined) + if (wcwidth === undefined) { wcwidth = lib.wc.strWidth(str); + } - if (this.textAttributes.matchesContainer(this.cursorNode_) && - this.cursorNode_.textContent.substr(this.cursorOffset_) == str) { + if (this.textAttributes.matchesContainer(lib.notNull(this.cursorNode_)) && + this.cursorNode_.textContent.substr(this.cursorOffset_) == + str) { // This overwrite would be a no-op, just move the cursor and return. this.cursorOffset_ += wcwidth; this.cursorPosition.column += wcwidth; @@ -9771,21 +11617,22 @@ hterm.Screen.prototype.overwriteString = function(str, wcwidth=undefined) { * Text to the right of the deleted characters is shifted left. Only affects * characters on the same row as the cursor. * - * @param {integer} count The column width of characters to delete. This is + * @param {number} count The column width of characters to delete. This is * clamped to the column width minus the cursor column. - * @return {integer} The column width of the characters actually deleted. + * @return {number} The column width of the characters actually deleted. */ hterm.Screen.prototype.deleteChars = function(count) { - var node = this.cursorNode_; - var offset = this.cursorOffset_; + let node = this.cursorNode_; + let offset = this.cursorOffset_; - var currentCursorColumn = this.cursorPosition.column; + const currentCursorColumn = this.cursorPosition.column; count = Math.min(count, this.columnCount_ - currentCursorColumn); - if (!count) + if (!count) { return 0; + } - var rv = count; - var startLength, endLength; + const rv = count; + let startLength, endLength; while (node && count) { // Sanity check so we don't loop forever, but we don't also go quietly. @@ -9805,21 +11652,23 @@ hterm.Screen.prototype.deleteChars = function(count) { // was deleted). If there are more chars to delete, the next loop will pick // up the slack. if (node.wcNode && offset < startLength && - ((endLength && startLength == endLength) || (!endLength && offset == 1))) { + ((endLength && startLength == endLength) || + (!endLength && offset == 1))) { // No characters were deleted when there should be. We're probably trying // to delete one column width from a wide character node. We remove the // wide character node here and replace it with a single space. - var spaceNode = this.textAttributes.createContainer(' '); + const spaceNode = this.textAttributes.createContainer(' '); node.parentNode.insertBefore(spaceNode, offset ? node : node.nextSibling); node.textContent = ''; endLength = 0; count -= 1; - } else + } else { count -= startLength - endLength; + } - var nextNode = node.nextSibling; + const nextNode = node.nextSibling; if (endLength == 0 && node != this.cursorNode_) { - node.parentNode.removeChild(node); + node.remove(); } node = nextNode; offset = 0; @@ -9828,7 +11677,7 @@ hterm.Screen.prototype.deleteChars = function(count) { // Remove this.cursorNode_ if it is an empty non-text node. if (this.cursorNode_.nodeType != Node.TEXT_NODE && !this.cursorNode_.textContent) { - var cursorNode = this.cursorNode_; + const cursorNode = this.cursorNode_; if (cursorNode.previousSibling) { this.cursorNode_ = cursorNode.previousSibling; this.cursorOffset_ = hterm.TextAttributes.nodeWidth( @@ -9837,7 +11686,7 @@ hterm.Screen.prototype.deleteChars = function(count) { this.cursorNode_ = cursorNode.nextSibling; this.cursorOffset_ = 0; } else { - var emptyNode = this.cursorRowNode_.ownerDocument.createTextNode(''); + const emptyNode = this.cursorRowNode_.ownerDocument.createTextNode(''); this.cursorRowNode_.appendChild(emptyNode); this.cursorNode_ = emptyNode; this.cursorOffset_ = 0; @@ -9852,8 +11701,8 @@ hterm.Screen.prototype.deleteChars = function(count) { * Finds first X-ROW of a line containing specified X-ROW. * Used to support line overflow. * - * @param {Node} row X-ROW to begin search for first row of line. - * @return {Node} The X-ROW that is at the beginning of the line. + * @param {!Node} row X-ROW to begin search for first row of line. + * @return {!Node} The X-ROW that is at the beginning of the line. **/ hterm.Screen.prototype.getLineStartRow_ = function(row) { while (row.previousSibling && @@ -9867,15 +11716,16 @@ hterm.Screen.prototype.getLineStartRow_ = function(row) { * Gets text of a line beginning with row. * Supports line overflow. * - * @param {Node} row First X-ROW of line. + * @param {!Node} row First X-ROW of line. * @return {string} Text content of line. **/ hterm.Screen.prototype.getLineText_ = function(row) { - var rowText = ""; - while (row) { - rowText += row.textContent; - if (row.hasAttribute('line-overflow')) { - row = row.nextSibling; + let rowText = ''; + let rowOrNull = row; + while (rowOrNull) { + rowText += rowOrNull.textContent; + if (rowOrNull.hasAttribute('line-overflow')) { + rowOrNull = rowOrNull.nextSibling; } else { break; } @@ -9886,35 +11736,38 @@ hterm.Screen.prototype.getLineText_ = function(row) { /** * Returns X-ROW that is ancestor of the node. * - * @param {Node} node Node to get X-ROW ancestor for. - * @return {Node} X-ROW ancestor of node, or null if not found. + * @param {!Node} node Node to get X-ROW ancestor for. + * @return {?Node} X-ROW ancestor of node, or null if not found. **/ hterm.Screen.prototype.getXRowAncestor_ = function(node) { - while (node) { - if (node.nodeName === 'X-ROW') + let nodeOrNull = node; + while (nodeOrNull) { + if (nodeOrNull.nodeName === 'X-ROW') { break; - node = node.parentNode; + } + nodeOrNull = nodeOrNull.parentNode; } - return node; + return nodeOrNull; }; /** * Returns position within line of character at offset within node. * Supports line overflow. * - * @param {Node} row X-ROW at beginning of line. - * @param {Node} node Node to get position of. - * @param {integer} offset Offset into node. - * - * @return {integer} Position within line of character at offset within node. + * @param {!Node} row X-ROW at beginning of line. + * @param {!Node} node Node to get position of. + * @param {number} offset Offset into node. + * @return {number} Position within line of character at offset within node. **/ hterm.Screen.prototype.getPositionWithOverflow_ = function(row, node, offset) { - if (!node) + if (!node) { return -1; - var ancestorRow = this.getXRowAncestor_(node); - if (!ancestorRow) + } + const ancestorRow = this.getXRowAncestor_(node); + if (!ancestorRow) { return -1; - var position = 0; + } + let position = 0; while (ancestorRow != row) { position += hterm.TextAttributes.nodeWidth(row); if (row.hasAttribute('line-overflow') && row.nextSibling) { @@ -9930,25 +11783,27 @@ hterm.Screen.prototype.getPositionWithOverflow_ = function(row, node, offset) { * Returns position within row of character at offset within node. * Does not support line overflow. * - * @param {Node} row X-ROW to get position within. - * @param {Node} node Node to get position for. - * @param {integer} offset Offset within node to get position for. - * @return {integer} Position within row of character at offset within node. + * @param {!Node} row X-ROW to get position within. + * @param {!Node} node Node to get position for. + * @param {number} offset Offset within node to get position for. + * @return {number} Position within row of character at offset within node. **/ hterm.Screen.prototype.getPositionWithinRow_ = function(row, node, offset) { if (node.parentNode != row) { // If we traversed to the top node, then there's nothing to find here. - if (node.parentNode == null) + if (node.parentNode == null) { return -1; + } return this.getPositionWithinRow_(node.parentNode, node, offset) + this.getPositionWithinRow_(row, node.parentNode, 0); } - var position = 0; - for (var i = 0; i < row.childNodes.length; i++) { - var currentNode = row.childNodes[i]; - if (currentNode == node) + let position = 0; + for (let i = 0; i < row.childNodes.length; i++) { + const currentNode = row.childNodes[i]; + if (currentNode == node) { return position + offset; + } position += hterm.TextAttributes.nodeWidth(currentNode); } return -1; @@ -9958,9 +11813,9 @@ hterm.Screen.prototype.getPositionWithinRow_ = function(row, node, offset) { * Returns the node and offset corresponding to position within line. * Supports line overflow. * - * @param {Node} row X-ROW at beginning of line. - * @param {integer} position Position within line to retrieve node and offset. - * @return {Array} Two element array containing node and offset respectively. + * @param {!Node} row X-ROW at beginning of line. + * @param {number} position Position within line to retrieve node and offset. + * @return {?Array} Two element array containing node and offset respectively. **/ hterm.Screen.prototype.getNodeAndOffsetWithOverflow_ = function(row, position) { while (row && position > hterm.TextAttributes.nodeWidth(row)) { @@ -9968,7 +11823,7 @@ hterm.Screen.prototype.getNodeAndOffsetWithOverflow_ = function(row, position) { position -= hterm.TextAttributes.nodeWidth(row); row = row.nextSibling; } else { - return -1; + return [null, -1]; } } return this.getNodeAndOffsetWithinRow_(row, position); @@ -9978,14 +11833,14 @@ hterm.Screen.prototype.getNodeAndOffsetWithOverflow_ = function(row, position) { * Returns the node and offset corresponding to position within row. * Does not support line overflow. * - * @param {Node} row X-ROW to get position within. - * @param {integer} position Position within row to retrieve node and offset. - * @return {Array} Two element array containing node and offset respectively. + * @param {!Node} row X-ROW to get position within. + * @param {number} position Position within row to retrieve node and offset. + * @return {?Array} Two element array containing node and offset respectively. **/ hterm.Screen.prototype.getNodeAndOffsetWithinRow_ = function(row, position) { - for (var i = 0; i < row.childNodes.length; i++) { - var node = row.childNodes[i]; - var nodeTextWidth = hterm.TextAttributes.nodeWidth(node); + for (let i = 0; i < row.childNodes.length; i++) { + const node = row.childNodes[i]; + const nodeTextWidth = hterm.TextAttributes.nodeWidth(node); if (position <= nodeTextWidth) { if (node.nodeName === 'SPAN') { /** Drill down to node contained by SPAN. **/ @@ -10003,18 +11858,20 @@ hterm.Screen.prototype.getNodeAndOffsetWithinRow_ = function(row, position) { * Returns the node and offset corresponding to position within line. * Supports line overflow. * - * @param {Node} row X-ROW at beginning of line. - * @param {integer} start Start position of range within line. - * @param {integer} end End position of range within line. - * @param {Range} range Range to modify. + * @param {!Node} row X-ROW at beginning of line. + * @param {number} start Start position of range within line. + * @param {number} end End position of range within line. + * @param {!Range} range Range to modify. **/ hterm.Screen.prototype.setRange_ = function(row, start, end, range) { - var startNodeAndOffset = this.getNodeAndOffsetWithOverflow_(row, start); - if (startNodeAndOffset == null) + const startNodeAndOffset = this.getNodeAndOffsetWithOverflow_(row, start); + if (startNodeAndOffset == null) { return; - var endNodeAndOffset = this.getNodeAndOffsetWithOverflow_(row, end); - if (endNodeAndOffset == null) + } + const endNodeAndOffset = this.getNodeAndOffsetWithOverflow_(row, end); + if (endNodeAndOffset == null) { return; + } range.setStart(startNodeAndOffset[0], startNodeAndOffset[1]); range.setEnd(endNodeAndOffset[0], endNodeAndOffset[1]); }; @@ -10022,56 +11879,63 @@ hterm.Screen.prototype.setRange_ = function(row, start, end, range) { /** * Expands selection to surrounding string with word break matches. * - * @param {Selection} selection Selection to expand. + * @param {?Selection} selection Selection to expand. * @param {string} leftMatch left word break match. * @param {string} rightMatch right word break match. * @param {string} insideMatch inside word break match. */ hterm.Screen.prototype.expandSelectionWithWordBreakMatches_ = function(selection, leftMatch, rightMatch, insideMatch) { - if (!selection) + if (!selection) { return; + } - var range = selection.getRangeAt(0); - if (!range || range.toString().match(/\s/)) + const range = selection.getRangeAt(0); + if (!range || range.toString().match(/\s/)) { return; + } - const rowElement = this.getXRowAncestor_(range.startContainer); - if (!rowElement) + const rowElement = this.getXRowAncestor_(lib.notNull(range.startContainer)); + if (!rowElement) { return; + } const row = this.getLineStartRow_(rowElement); - if (!row) + if (!row) { return; + } - var startPosition = this.getPositionWithOverflow_(row, - range.startContainer, - range.startOffset); - if (startPosition == -1) + const startPosition = this.getPositionWithOverflow_( + row, lib.notNull(range.startContainer), range.startOffset); + if (startPosition == -1) { return; - var endPosition = this.getPositionWithOverflow_(row, - range.endContainer, - range.endOffset); - if (endPosition == -1) + } + const endPosition = this.getPositionWithOverflow_( + row, lib.notNull(range.endContainer), range.endOffset); + if (endPosition == -1) { return; + } - //Move start to the left. - var rowText = this.getLineText_(row); - var lineUpToRange = lib.wc.substring(rowText, 0, endPosition); - var leftRegularExpression = new RegExp(leftMatch + insideMatch + "$"); - var expandedStart = lineUpToRange.search(leftRegularExpression); - if (expandedStart == -1 || expandedStart > startPosition) + // Move start to the left. + const rowText = this.getLineText_(row); + const lineUpToRange = lib.wc.substring(rowText, 0, endPosition); + const leftRegularExpression = new RegExp(leftMatch + insideMatch + '$'); + const expandedStart = lineUpToRange.search(leftRegularExpression); + if (expandedStart == -1 || expandedStart > startPosition) { return; + } - //Move end to the right. - var lineFromRange = lib.wc.substring(rowText, startPosition, - lib.wc.strWidth(rowText)); - var rightRegularExpression = new RegExp("^" + insideMatch + rightMatch); - var found = lineFromRange.match(rightRegularExpression); - if (!found) + // Move end to the right. + const lineFromRange = lib.wc.substring(rowText, startPosition, + lib.wc.strWidth(rowText)); + const rightRegularExpression = new RegExp('^' + insideMatch + rightMatch); + const found = lineFromRange.match(rightRegularExpression); + if (!found) { return; - var expandedEnd = startPosition + lib.wc.strWidth(found[0]); - if (expandedEnd == -1 || expandedEnd < endPosition) + } + const expandedEnd = startPosition + lib.wc.strWidth(found[0]); + if (expandedEnd == -1 || expandedEnd < endPosition) { return; + } this.setRange_(row, expandedStart, expandedEnd, range); selection.addRange(range); @@ -10080,33 +11944,33 @@ hterm.Screen.prototype.expandSelectionWithWordBreakMatches_ = /** * Expands selection to surrounding string using the user's settings. * - * @param {Selection} selection Selection to expand. + * @param {?Selection} selection Selection to expand. */ hterm.Screen.prototype.expandSelection = function(selection) { this.expandSelectionWithWordBreakMatches_( selection, - this.wordBreakMatchLeft, - this.wordBreakMatchRight, - this.wordBreakMatchMiddle); + lib.notNull(this.wordBreakMatchLeft), + lib.notNull(this.wordBreakMatchRight), + lib.notNull(this.wordBreakMatchMiddle)); }; /** * Expands selection to surrounding URL using a set of fixed match settings. * - * @param {Selection} selection Selection to expand. + * @param {?Selection} selection Selection to expand. */ hterm.Screen.prototype.expandSelectionForUrl = function(selection) { this.expandSelectionWithWordBreakMatches_( selection, - "[^\\s\\[\\](){}<>\"'\\^!@#$%&*,;:`]", - "[^\\s\\[\\](){}<>\"'\\^!@#$%&*,;:~.`]", - "[^\\s\\[\\](){}<>\"'\\^]*"); + '[^\\s[\\](){}<>"\'^!@#$%&*,;:`\u{2018}\u{201c}\u{2039}\u{ab}]', + '[^\\s[\\](){}<>"\'^!@#$%&*,;:~.`\u{2019}\u{201d}\u{203a}\u{bb}]', + '[^\\s[\\](){}<>"\'^]*'); }; /** * Save the current cursor state to the corresponding screens. * - * @param {hterm.VT} vt The VT object to read graphic codeset details from. + * @param {!hterm.VT} vt The VT object to read graphic codeset details from. */ hterm.Screen.prototype.saveCursorAndState = function(vt) { this.cursorState_.save(vt); @@ -10115,7 +11979,7 @@ hterm.Screen.prototype.saveCursorAndState = function(vt) { /** * Restore the saved cursor state in the corresponding screens. * - * @param {hterm.VT} vt The VT object to write graphic codeset details to. + * @param {!hterm.VT} vt The VT object to write graphic codeset details to. */ hterm.Screen.prototype.restoreCursorAndState = function(vt) { this.cursorState_.restore(vt); @@ -10135,6 +11999,9 @@ hterm.Screen.prototype.restoreCursorAndState = function(vt) { * - Any single shift 2 (SS2) or single shift 3 (SS3) functions sent * * These are done on a per-screen basis. + * + * @param {!hterm.Screen} screen The screen this cursor is tied to. + * @constructor */ hterm.Screen.CursorState = function(screen) { this.screen_ = screen; @@ -10146,7 +12013,7 @@ hterm.Screen.CursorState = function(screen) { /** * Save all the cursor state. * - * @param {hterm.VT} vt The VT object to read graphic codeset details from. + * @param {!hterm.VT} vt The VT object to read graphic codeset details from. */ hterm.Screen.CursorState.prototype.save = function(vt) { this.cursor = vt.terminal.saveCursor(); @@ -10165,7 +12032,7 @@ hterm.Screen.CursorState.prototype.save = function(vt) { /** * Restore the previously saved cursor state. * - * @param {hterm.VT} vt The VT object to write graphic codeset details to. + * @param {!hterm.VT} vt The VT object to write graphic codeset details to. */ hterm.Screen.CursorState.prototype.restore = function(vt) { vt.terminal.restoreCursor(this.cursor); @@ -10173,7 +12040,8 @@ hterm.Screen.CursorState.prototype.restore = function(vt) { // Cursor restore includes char attributes (bold/etc...), but does not change // the color palette (which are a terminal setting). const tattrs = this.textAttributes.clone(); - tattrs.colorPalette = this.screen_.textAttributes.colorPalette; + tattrs.colorPaletteOverrides = + this.screen_.textAttributes.colorPaletteOverrides; tattrs.syncColors(); this.screen_.textAttributes = tattrs; @@ -10191,6 +12059,30 @@ hterm.Screen.CursorState.prototype.restore = function(vt) { // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. +/** + * The RowProvider should return rows rooted by the custom tag name 'x-row'. + * This ensures that we can quickly assign the correct display height + * to the rows with css. + * + * @interface + */ +hterm.RowProvider = function() {}; + +/** + * @abstract + * @return {number} The current number of rows. + */ +hterm.RowProvider.prototype.getRowCount = function() {}; + +/** + * Get specified row. + * + * @abstract + * @param {number} index The index of the row. + * @return {!Element} + */ +hterm.RowProvider.prototype.getRowNode = function(index) {}; + /** * A 'viewport' view of fixed-height rows with support for selection and * copy-to-clipboard. @@ -10208,12 +12100,10 @@ hterm.Screen.CursorState.prototype.restore = function(vt) { * of the selection is off screen. It would be difficult to fix this without * adding significant overhead to pathologically large selection cases. * - * The RowProvider should return rows rooted by the custom tag name 'x-row'. - * This ensures that we can quickly assign the correct display height - * to the rows with css. - * - * @param {RowProvider} rowProvider An object capable of providing rows as - * raw text or row nodes. + * @param {!hterm.RowProvider} rowProvider An object capable of providing rows + * as raw text or row nodes. + * @constructor + * @extends {hterm.PubSub} */ hterm.ScrollPort = function(rowProvider) { hterm.PubSub.addBehavior(this); @@ -10257,6 +12147,11 @@ hterm.ScrollPort = function(rowProvider) { // by touch identifier since we can have more than one touch active. this.lastTouch_ = {}; + /** + * Size of screen padding in pixels. + */ + this.screenPaddingSize = 0; + /** * True if the last scroll caused the scrollport to show the final row. */ @@ -10265,7 +12160,7 @@ hterm.ScrollPort = function(rowProvider) { /** * A guess at the current scrollbar width, fixed in resize(). */ - this.currentScrollbarWidthPx = 16; + this.currentScrollbarWidthPx = hterm.ScrollPort.DEFAULT_SCROLLBAR_WIDTH; /** * Whether the ctrl-v key on the screen should paste. @@ -10279,6 +12174,8 @@ hterm.ScrollPort = function(rowProvider) { this.div_ = null; this.document_ = null; + /** @type {?Element} */ + this.screen_ = null; // Collection of active timeout handles. this.timeouts_ = {}; @@ -10292,11 +12189,20 @@ hterm.ScrollPort = function(rowProvider) { this.DEBUG_ = false; }; +/** + * Default width for scrollbar used when the system such as CrOS pretends that + * scrollbar is zero width. CrOS currently uses 11px when expanded. + * + * @const {number} + */ +hterm.ScrollPort.DEFAULT_SCROLLBAR_WIDTH = 12; + /** * Proxy for the native selection object which understands how to walk up the * DOM to find the containing row node and sort out which comes first. * - * @param {hterm.ScrollPort} scrollPort The parent hterm.ScrollPort instance. + * @param {!hterm.ScrollPort} scrollPort The parent hterm.ScrollPort instance. + * @constructor */ hterm.ScrollPort.Selection = function(scrollPort) { this.scrollPort_ = scrollPort; @@ -10340,20 +12246,24 @@ hterm.ScrollPort.Selection = function(scrollPort) { * Given a list of DOM nodes and a container, return the DOM node that * is first according to a depth-first search. * - * Returns null if none of the children are found. + * @param {!Node} parent + * @param {!Array} childAry + * @return {?Node} Returns null if none of the children are found. */ hterm.ScrollPort.Selection.prototype.findFirstChild = function( parent, childAry) { - var node = parent.firstChild; + let node = parent.firstChild; while (node) { - if (childAry.indexOf(node) != -1) + if (childAry.indexOf(node) != -1) { return node; + } if (node.childNodes.length) { - var rv = this.findFirstChild(node, childAry); - if (rv) + const rv = this.findFirstChild(node, childAry); + if (rv) { return rv; + } } node = node.nextSibling; @@ -10369,32 +12279,30 @@ hterm.ScrollPort.Selection.prototype.findFirstChild = function( * object, not the other way around. */ hterm.ScrollPort.Selection.prototype.sync = function() { - var self = this; - // The dom selection object has no way to tell which nodes come first in // the document, so we have to figure that out. // // This function is used when we detect that the "anchor" node is first. - function anchorFirst() { - self.startRow = anchorRow; - self.startNode = selection.anchorNode; - self.startOffset = selection.anchorOffset; - self.endRow = focusRow; - self.endNode = selection.focusNode; - self.endOffset = selection.focusOffset; - } + const anchorFirst = () => { + this.startRow = anchorRow; + this.startNode = selection.anchorNode; + this.startOffset = selection.anchorOffset; + this.endRow = focusRow; + this.endNode = selection.focusNode; + this.endOffset = selection.focusOffset; + }; // This function is used when we detect that the "focus" node is first. - function focusFirst() { - self.startRow = focusRow; - self.startNode = selection.focusNode; - self.startOffset = selection.focusOffset; - self.endRow = anchorRow; - self.endNode = selection.anchorNode; - self.endOffset = selection.anchorOffset; - } + const focusFirst = () => { + this.startRow = focusRow; + this.startNode = selection.focusNode; + this.startOffset = selection.focusOffset; + this.endRow = anchorRow; + this.endNode = selection.anchorNode; + this.endOffset = selection.anchorOffset; + }; - var selection = this.scrollPort_.getDocument().getSelection(); + const selection = this.scrollPort_.getDocument().getSelection(); this.startRow = null; this.endRow = null; @@ -10415,7 +12323,7 @@ hterm.ScrollPort.Selection.prototype.sync = function() { return; } - var anchorRow = selection.anchorNode; + let anchorRow = selection.anchorNode; while (anchorRow && anchorRow.nodeName != 'X-ROW') { anchorRow = anchorRow.parentNode; } @@ -10425,7 +12333,7 @@ hterm.ScrollPort.Selection.prototype.sync = function() { return; } - var focusRow = selection.focusNode; + let focusRow = selection.focusNode; while (focusRow && focusRow.nodeName != 'X-ROW') { focusRow = focusRow.parentNode; } @@ -10451,11 +12359,12 @@ hterm.ScrollPort.Selection.prototype.sync = function() { } else { // The selection starts and ends in the same row, but isn't contained all // in a single node. - var firstNode = this.findFirstChild( + const firstNode = this.findFirstChild( anchorRow, [selection.anchorNode, selection.focusNode]); - if (!firstNode) + if (!firstNode) { throw new Error('Unexpected error syncing selection.'); + } if (firstNode == selection.anchorNode) { anchorFirst(); @@ -10467,9 +12376,11 @@ hterm.ScrollPort.Selection.prototype.sync = function() { this.isMultiline = anchorRow.rowIndex != focusRow.rowIndex; }; - /** * Turn a div into this hterm.ScrollPort. + * + * @param {!Element} div + * @param {function()=} callback */ hterm.ScrollPort.prototype.decorate = function(div, callback) { this.div_ = div; @@ -10503,13 +12414,14 @@ hterm.ScrollPort.prototype.decorate = function(div, callback) { /** * Initialises the content of this.iframe_. This needs to be done asynchronously * in FF after the Iframe's load event has fired. + * * @private */ hterm.ScrollPort.prototype.paintIframeContents_ = function() { this.iframe_.contentWindow.addEventListener('resize', this.onResize_.bind(this)); - var doc = this.document_ = this.iframe_.contentDocument; + const doc = this.document_ = this.iframe_.contentDocument; doc.body.style.cssText = ( 'margin: 0px;' + 'padding: 0px;' + @@ -10517,8 +12429,7 @@ hterm.ScrollPort.prototype.paintIframeContents_ = function() { 'width: 100%;' + 'overflow: hidden;' + 'cursor: var(--hterm-mouse-cursor-style);' + - '-webkit-user-select: none;' + - '-moz-user-select: none;'); + 'user-select: none;'); const metaCharset = doc.createElement('meta'); metaCharset.setAttribute('charset', 'utf-8'); @@ -10532,7 +12443,7 @@ hterm.ScrollPort.prototype.paintIframeContents_ = function() { 'calc(var(--hterm-charsize-height) * 3)'; } - var style = doc.createElement('style'); + const style = doc.createElement('style'); style.textContent = ( 'x-row {' + ' display: block;' + @@ -10581,49 +12492,60 @@ hterm.ScrollPort.prototype.paintIframeContents_ = function() { // orthogonal to the DOM's notion of modifiable. this.screen_.setAttribute('aria-readonly', 'true'); this.screen_.setAttribute('tabindex', '-1'); - this.screen_.style.cssText = ( - 'caret-color: transparent;' + - 'display: block;' + - 'font-family: monospace;' + - 'font-size: 15px;' + - 'font-variant-ligatures: none;' + - 'height: 100%;' + - 'overflow-y: scroll; overflow-x: hidden;' + - 'white-space: pre;' + - 'width: 100%;' + - 'outline: none !important'); + this.screen_.style.cssText = ` + background-color: rgb(var(--hterm-background-color)); + caret-color: transparent; + color: rgb(var(--hterm-foreground-color)); + display: block; + font-family: monospace; + font-size: 15px; + font-variant-ligatures: none; + height: 100%; + overflow-y: scroll; overflow-x: hidden; + white-space: pre; + width: 100%; + outline: none !important; + `; - doc.body.appendChild(this.screen_); - this.screen_.addEventListener('scroll', this.onScroll_.bind(this)); - this.screen_.addEventListener('wheel', this.onScrollWheel_.bind(this)); - this.screen_.addEventListener('touchstart', this.onTouch_.bind(this)); - this.screen_.addEventListener('touchmove', this.onTouch_.bind(this)); - this.screen_.addEventListener('touchend', this.onTouch_.bind(this)); - this.screen_.addEventListener('touchcancel', this.onTouch_.bind(this)); - this.screen_.addEventListener('copy', this.onCopy_.bind(this)); - this.screen_.addEventListener('paste', this.onPaste_.bind(this)); - this.screen_.addEventListener('drop', this.onDragAndDrop_.bind(this)); + /** + * @param {function(...)} f + * @return {!EventListener} + */ + const el = (f) => /** @type {!EventListener} */ (f); + this.screen_.addEventListener('scroll', el(this.onScroll_.bind(this))); + this.screen_.addEventListener('wheel', el(this.onScrollWheel_.bind(this))); + this.screen_.addEventListener('touchstart', el(this.onTouch_.bind(this))); + this.screen_.addEventListener('touchmove', el(this.onTouch_.bind(this))); + this.screen_.addEventListener('touchend', el(this.onTouch_.bind(this))); + this.screen_.addEventListener('touchcancel', el(this.onTouch_.bind(this))); + this.screen_.addEventListener('copy', el(this.onCopy_.bind(this))); + this.screen_.addEventListener('paste', el(this.onPaste_.bind(this))); + this.screen_.addEventListener('drop', el(this.onDragAndDrop_.bind(this))); doc.body.addEventListener('keydown', this.onBodyKeyDown_.bind(this)); // Add buttons to make accessible scrolling through terminal history work // well. These are positioned off-screen until they are selected, at which // point they are moved on-screen. - const scrollButtonHeight = 30; - const scrollButtonBorder = 1; - const scrollButtonTotalHeight = scrollButtonHeight + 2 * scrollButtonBorder; - const scrollButtonStyle = `right: 0px; - position:fixed; - z-index: 1; - text-align: center; - cursor: pointer; - height: ${scrollButtonHeight}px; - width: 110px; - line-height: ${scrollButtonHeight}px; - border-width: ${scrollButtonBorder}px; - border-style: solid; - font-weight: bold;`; + const a11yButtonHeight = 30; + const a11yButtonBorder = 1; + const a11yButtonTotalHeight = a11yButtonHeight + 2 * a11yButtonBorder; + const a11yButtonStyle = ` + border-style: solid; + border-width: ${a11yButtonBorder}px; + color: rgb(var(--hterm-foreground-color)); + cursor: pointer; + font-family: monospace; + font-weight: bold; + height: ${a11yButtonHeight}px; + line-height: ${a11yButtonHeight}px; + padding: 0 8px; + position: fixed; + right: var(--hterm-screen-padding-size); + text-align: center; + z-index: 1; + `; // Note: we use a
rather than a