blob: 8ef1de6574103b0b3044552c76ba5478064a0aaa [file] [log] [blame]
// 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.rtdep('lib.colors');
/**
* Constructor for TextAttribute objects.
*
* These objects manage a set of text attributes such as foreground/
* background color, bold, faint, italic, blink, underline, and strikethrough.
*
* TextAttribute instances can be used to construct a DOM container implementing
* the current attributes, or to test an existing DOM container for
* compatibility with the current attributes.
*
* @constructor
* @param {HTMLDocument} document The parent document to use when creating
* new DOM containers.
*/
hterm.TextAttributes = function(document) {
this.document_ = document;
// These variables contain the source of the color as either:
// SRC_DEFAULT (use context default)
// rgb(...) (true color form)
// number (representing the index from color palette to use)
this.foregroundSource = this.SRC_DEFAULT;
this.backgroundSource = this.SRC_DEFAULT;
this.underlineSource = this.SRC_DEFAULT;
// These properties cache the value in the color table, but foregroundSource
// and backgroundSource contain the canonical values.
this.foreground = this.DEFAULT_COLOR;
this.background = this.DEFAULT_COLOR;
this.underlineColor = this.DEFAULT_COLOR;
this.defaultForeground = 'rgb(255, 255, 255)';
this.defaultBackground = 'rgb(0, 0, 0)';
// Any attributes added here that do not default to falsey (e.g. undefined or
// null) require a bit more care. createContainer has to always attach the
// attribute so matchesContainer can work correctly.
this.bold = false;
this.faint = false;
this.italic = false;
this.blink = false;
this.underline = false;
this.strikethrough = false;
this.inverse = false;
this.invisible = false;
this.wcNode = false;
this.asciiNode = true;
this.tileData = null;
this.uri = null;
this.uriId = null;
this.colorPalette = null;
this.resetColorPalette();
};
/**
* If false, we ignore the bold attribute.
*
* This is used for fonts that have a bold version that is a different size
* than the normal weight version.
*/
hterm.TextAttributes.prototype.enableBold = true;
/**
* If true, use bright colors (if available) for bold text.
*
* This setting is independent of the enableBold setting.
*/
hterm.TextAttributes.prototype.enableBoldAsBright = true;
/**
* A sentinel constant meaning "whatever the default color is in this context".
*/
hterm.TextAttributes.prototype.DEFAULT_COLOR = lib.f.createEnum('');
/**
* A constant string used to specify that source color is context default.
*/
hterm.TextAttributes.prototype.SRC_DEFAULT = 'default';
/**
* The document object which should own the DOM nodes created by this instance.
*
* @param {HTMLDocument} document The parent document.
*/
hterm.TextAttributes.prototype.setDocument = function(document) {
this.document_ = document;
};
/**
* Create a deep copy of this object.
*
* @return {hterm.TextAttributes} A deep copy of this object.
*/
hterm.TextAttributes.prototype.clone = function() {
var rv = new hterm.TextAttributes(null);
for (var key in this) {
rv[key] = this[key];
}
rv.colorPalette = this.colorPalette.concat();
return rv;
};
/**
* Reset the current set of attributes.
*
* This does not affect the palette. Use resetColorPalette() for that.
* It also doesn't affect the tile data, it's not meant to.
*/
hterm.TextAttributes.prototype.reset = function() {
this.foregroundSource = this.SRC_DEFAULT;
this.backgroundSource = this.SRC_DEFAULT;
this.underlineSource = this.SRC_DEFAULT;
this.foreground = this.DEFAULT_COLOR;
this.background = this.DEFAULT_COLOR;
this.underlineColor = this.DEFAULT_COLOR;
this.bold = false;
this.faint = false;
this.italic = false;
this.blink = false;
this.underline = false;
this.strikethrough = false;
this.inverse = false;
this.invisible = false;
this.wcNode = false;
this.asciiNode = true;
this.uri = null;
this.uriId = null;
};
/**
* Reset the color palette to the default state.
*/
hterm.TextAttributes.prototype.resetColorPalette = function() {
this.colorPalette = lib.colors.colorPalette.concat();
this.syncColors();
};
/**
* Test if the current attributes describe unstyled text.
*
* @return {boolean} True if the current attributes describe unstyled text.
*/
hterm.TextAttributes.prototype.isDefault = function() {
return (this.foregroundSource == this.SRC_DEFAULT &&
this.backgroundSource == this.SRC_DEFAULT &&
!this.bold &&
!this.faint &&
!this.italic &&
!this.blink &&
!this.underline &&
!this.strikethrough &&
!this.inverse &&
!this.invisible &&
!this.wcNode &&
this.asciiNode &&
this.tileData == null &&
this.uri == null);
};
/**
* Create a DOM container (a span or a text node) with a style to match the
* current set of attributes.
*
* This method will create a plain text node if the text is unstyled, or
* an HTML span if the text is styled. Due to lack of monospace wide character
* fonts on certain systems (e.g. Chrome OS), we need to put each wide character
* in a span of CSS class '.wc-node' which has double column width.
* Each vt_tiledata tile is also represented by a span with a single
* character, with CSS classes '.tile' and '.tile_<glyph number>'.
*
* @param {string} opt_textContent Optional text content for the new container.
* @return {HTMLNode} An HTML span or text nodes styled to match the current
* attributes.
*/
hterm.TextAttributes.prototype.createContainer = function(opt_textContent) {
if (this.isDefault()) {
// Only attach attributes where we need an explicit default for the
// matchContainer logic below.
const node = this.document_.createTextNode(opt_textContent);
node.asciiNode = true;
return node;
}
var span = this.document_.createElement('span');
var style = span.style;
var classes = [];
if (this.foreground != this.DEFAULT_COLOR)
style.color = this.foreground;
if (this.background != this.DEFAULT_COLOR)
style.backgroundColor = this.background;
if (this.enableBold && this.bold)
style.fontWeight = 'bold';
if (this.faint)
span.faint = true;
if (this.italic)
style.fontStyle = 'italic';
if (this.blink) {
classes.push('blink-node');
span.blinkNode = true;
}
let textDecorationLine = '';
span.underline = this.underline;
if (this.underline) {
textDecorationLine += ' underline';
style.textDecorationStyle = this.underline;
}
if (this.underlineSource != this.SRC_DEFAULT)
style.textDecorationColor = this.underlineColor;
if (this.strikethrough) {
textDecorationLine += ' line-through';
span.strikethrough = true;
}
if (textDecorationLine)
style.textDecorationLine = textDecorationLine;
if (this.wcNode) {
classes.push('wc-node');
span.wcNode = true;
}
span.asciiNode = this.asciiNode;
if (this.tileData != null) {
classes.push('tile');
classes.push('tile_' + this.tileData);
span.tileNode = true;
}
if (opt_textContent)
span.textContent = opt_textContent;
if (this.uri) {
classes.push('uri-node');
span.uriId = this.uriId;
span.title = this.uri;
span.addEventListener('click', hterm.openUrl.bind(this, this.uri));
}
if (classes.length)
span.className = classes.join(' ');
return span;
};
/**
* Tests if the provided object (string, span or text node) has the same
* style as this TextAttributes instance.
*
* This indicates that text with these attributes could be inserted directly
* into the target DOM node.
*
* For the purposes of this method, a string is considered a text node.
*
* @param {string|HTMLNode} obj The object to test.
* @return {boolean} True if the provided container has the same style as
* this attributes instance.
*/
hterm.TextAttributes.prototype.matchesContainer = function(obj) {
if (typeof obj == 'string' || obj.nodeType == Node.TEXT_NODE)
return this.isDefault();
var style = obj.style;
// We don't want to put multiple characters in a wcNode or a tile.
// See the comments in createContainer.
// For attributes that default to false, we do not require that obj have them
// declared, so always normalize them using !! (to turn undefined into false)
// in the compares below.
return (!(this.wcNode || obj.wcNode) &&
this.asciiNode == obj.asciiNode &&
!(this.tileData != null || obj.tileNode) &&
this.uriId == obj.uriId &&
this.foreground == style.color &&
this.background == style.backgroundColor &&
this.underlineColor == style.textDecorationColor &&
(this.enableBold && this.bold) == !!style.fontWeight &&
this.blink == !!obj.blinkNode &&
this.italic == !!style.fontStyle &&
this.underline == obj.underline &&
!!this.strikethrough == !!obj.strikethrough);
};
hterm.TextAttributes.prototype.setDefaults = function(foreground, background) {
this.defaultForeground = foreground;
this.defaultBackground = background;
this.syncColors();
};
/**
* Updates foreground and background properties based on current indices and
* other state.
*
* @param {string} terminalForeground The terminal foreground color for use as
* inverse text background.
* @param {string} terminalBackground The terminal background color for use as
* inverse text foreground.
*
*/
hterm.TextAttributes.prototype.syncColors = function() {
function getBrightIndex(i) {
if (i < 8) {
// If the color is from the lower half of the ANSI 16, add 8.
return i + 8;
}
// If it's not from the 16 color palette, ignore bold requests. This
// matches the behavior of gnome-terminal.
return i;
}
var foregroundSource = this.foregroundSource;
var backgroundSource = this.backgroundSource;
var defaultForeground = this.DEFAULT_COLOR;
var defaultBackground = this.DEFAULT_COLOR;
if (this.inverse) {
foregroundSource = this.backgroundSource;
backgroundSource = this.foregroundSource;
// We can't inherit the container's color anymore.
defaultForeground = this.defaultBackground;
defaultBackground = this.defaultForeground;
}
if (this.enableBoldAsBright && this.bold) {
if (Number.isInteger(foregroundSource)) {
foregroundSource = getBrightIndex(foregroundSource);
}
}
if (foregroundSource == this.SRC_DEFAULT)
this.foreground = defaultForeground;
else if (Number.isInteger(foregroundSource))
this.foreground = this.colorPalette[foregroundSource];
else
this.foreground = foregroundSource;
if (this.faint) {
var colorToMakeFaint = ((this.foreground == this.DEFAULT_COLOR) ?
this.defaultForeground : this.foreground);
this.foreground = lib.colors.mix(colorToMakeFaint, 'rgb(0, 0, 0)', 0.3333);
}
if (backgroundSource == this.SRC_DEFAULT)
this.background = defaultBackground;
else if (Number.isInteger(backgroundSource))
this.background = this.colorPalette[backgroundSource];
else
this.background = backgroundSource;
// Process invisible settings last to keep it simple.
if (this.invisible)
this.foreground = this.background;
if (this.underlineSource == this.SRC_DEFAULT)
this.underlineColor = '';
else if (Number.isInteger(this.underlineSource))
this.underlineColor = this.colorPalette[this.underlineSource];
else
this.underlineColor = this.underlineSource;
};
/**
* Static method used to test if the provided objects (strings, spans or
* text nodes) have the same style.
*
* For the purposes of this method, a string is considered a text node.
*
* @param {string|HTMLNode} obj1 An object to test.
* @param {string|HTMLNode} obj2 Another object to test.
* @return {boolean} True if the containers have the same style.
*/
hterm.TextAttributes.containersMatch = function(obj1, obj2) {
if (typeof obj1 == 'string')
return hterm.TextAttributes.containerIsDefault(obj2);
if (obj1.nodeType != obj2.nodeType)
return false;
if (obj1.nodeType == Node.TEXT_NODE)
return true;
var style1 = obj1.style;
var style2 = obj2.style;
return (style1.color == style2.color &&
style1.backgroundColor == style2.backgroundColor &&
style1.backgroundColor == style2.backgroundColor &&
style1.fontWeight == style2.fontWeight &&
style1.fontStyle == style2.fontStyle &&
style1.textDecoration == style2.textDecoration &&
style1.textDecorationColor == style2.textDecorationColor &&
style1.textDecorationStyle == style2.textDecorationStyle &&
style1.textDecorationLine == style2.textDecorationLine);
};
/**
* Static method to test if a given DOM container represents unstyled text.
*
* For the purposes of this method, a string is considered a text node.
*
* @param {string|HTMLNode} obj1 An object to test.
* @return {boolean} True if the object is unstyled.
*/
hterm.TextAttributes.containerIsDefault = function(obj) {
return typeof obj == 'string' || obj.nodeType == Node.TEXT_NODE;
};
/**
* Static method to get the column width of a node's textContent.
*
* @param {HTMLElement} node The HTML element to get the width of textContent
* from.
* @return {integer} The column width of the node's textContent.
*/
hterm.TextAttributes.nodeWidth = function(node) {
if (!node.asciiNode) {
return lib.wc.strWidth(node.textContent);
} else {
return node.textContent.length;
}
}
/**
* Static method to get the substr of a node's textContent. The start index
* and substr width are computed in column width.
*
* @param {HTMLElement} node The HTML element to get the substr of textContent
* from.
* @param {integer} start The starting offset in column width.
* @param {integer} width The width to capture in column width.
* @return {integer} The extracted substr of the node's textContent.
*/
hterm.TextAttributes.nodeSubstr = function(node, start, width) {
if (!node.asciiNode) {
return lib.wc.substr(node.textContent, start, width);
} else {
return node.textContent.substr(start, width);
}
}
/**
* Static method to get the substring based of a node's textContent. The
* start index of end index are computed in column width.
*
* @param {HTMLElement} node The HTML element to get the substr of textContent
* from.
* @param {integer} start The starting offset in column width.
* @param {integer} end The ending offset in column width.
* @return {integer} The extracted substring of the node's textContent.
*/
hterm.TextAttributes.nodeSubstring = function(node, start, end) {
if (!node.asciiNode) {
return lib.wc.substring(node.textContent, start, end);
} else {
return node.textContent.substring(start, end);
}
};
/**
* Static method to split a string into contiguous runs of single-width
* characters and runs of double-width characters.
*
* @param {string} str The string to split.
* @return {Array} An array of objects that contain substrings of str, where
* each substring is either a contiguous runs of single-width characters
* or a double-width character. For objects that contain a double-width
* character, its wcNode property is set to true. For objects that contain
* only ASCII content, its asciiNode property is set to true.
*/
hterm.TextAttributes.splitWidecharString = function(str) {
var rv = [];
var base = 0, length = 0, wcStrWidth = 0, wcCharWidth;
var asciiNode = true;
for (var i = 0; i < str.length;) {
var c = str.codePointAt(i);
var increment;
if (c < 128) {
wcStrWidth += 1;
length += 1;
increment = 1;
} else {
increment = (c <= 0xffff) ? 1 : 2;
wcCharWidth = lib.wc.charWidth(c);
if (wcCharWidth <= 1) {
wcStrWidth += wcCharWidth;
length += increment;
asciiNode = false;
} else {
if (length) {
rv.push({
str: str.substr(base, length),
asciiNode: asciiNode,
wcStrWidth: wcStrWidth,
});
asciiNode = true;
wcStrWidth = 0;
}
rv.push({
str: str.substr(i, increment),
wcNode: true,
asciiNode: false,
wcStrWidth: 2,
});
base = i + increment;
length = 0;
}
}
i += increment;
}
if (length) {
rv.push({
str: str.substr(base, length),
asciiNode: asciiNode,
wcStrWidth: wcStrWidth,
});
}
return rv;
};