blob: 36fe7eb6ae66f917ad00c232c388a623cc945897 [file] [log] [blame]
// Copyright 2016 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
/**
* @unrestricted
*/
export default class CSSProperty {
/**
* @param {!SDK.CSSStyleDeclaration} ownerStyle
* @param {number} index
* @param {string} name
* @param {string} value
* @param {boolean} important
* @param {boolean} disabled
* @param {boolean} parsedOk
* @param {boolean} implicit
* @param {?string=} text
* @param {!Protocol.CSS.SourceRange=} range
*/
constructor(ownerStyle, index, name, value, important, disabled, parsedOk, implicit, text, range) {
this.ownerStyle = ownerStyle;
this.index = index;
this.name = name;
this.value = value;
this.important = important;
this.disabled = disabled;
this.parsedOk = parsedOk;
this.implicit = implicit; // A longhand, implicitly set by missing values of shorthand.
this.text = text;
this.range = range ? TextUtils.TextRange.fromObject(range) : null;
this._active = true;
this._nameRange = null;
this._valueRange = null;
}
/**
* @param {!SDK.CSSStyleDeclaration} ownerStyle
* @param {number} index
* @param {!Protocol.CSS.CSSProperty} payload
* @return {!CSSProperty}
*/
static parsePayload(ownerStyle, index, payload) {
// The following default field values are used in the payload:
// important: false
// parsedOk: true
// implicit: false
// disabled: false
const result = new CSSProperty(
ownerStyle, index, payload.name, payload.value, payload.important || false, payload.disabled || false,
('parsedOk' in payload) ? !!payload.parsedOk : true, !!payload.implicit, payload.text, payload.range);
return result;
}
_ensureRanges() {
if (this._nameRange && this._valueRange) {
return;
}
const range = this.range;
const text = this.text ? new TextUtils.Text(this.text) : null;
if (!range || !text) {
return;
}
const nameIndex = text.value().indexOf(this.name);
const valueIndex = text.value().lastIndexOf(this.value);
if (nameIndex === -1 || valueIndex === -1 || nameIndex > valueIndex) {
return;
}
const nameSourceRange = new TextUtils.SourceRange(nameIndex, this.name.length);
const valueSourceRange = new TextUtils.SourceRange(valueIndex, this.value.length);
this._nameRange = rebase(text.toTextRange(nameSourceRange), range.startLine, range.startColumn);
this._valueRange = rebase(text.toTextRange(valueSourceRange), range.startLine, range.startColumn);
/**
* @param {!TextUtils.TextRange} oneLineRange
* @param {number} lineOffset
* @param {number} columnOffset
* @return {!TextUtils.TextRange}
*/
function rebase(oneLineRange, lineOffset, columnOffset) {
if (oneLineRange.startLine === 0) {
oneLineRange.startColumn += columnOffset;
oneLineRange.endColumn += columnOffset;
}
oneLineRange.startLine += lineOffset;
oneLineRange.endLine += lineOffset;
return oneLineRange;
}
}
/**
* @return {?TextUtils.TextRange}
*/
nameRange() {
this._ensureRanges();
return this._nameRange;
}
/**
* @return {?TextUtils.TextRange}
*/
valueRange() {
this._ensureRanges();
return this._valueRange;
}
/**
* @param {!SDK.CSSModel.Edit} edit
*/
rebase(edit) {
if (this.ownerStyle.styleSheetId !== edit.styleSheetId) {
return;
}
if (this.range) {
this.range = this.range.rebaseAfterTextEdit(edit.oldRange, edit.newRange);
}
}
/**
* @param {boolean} active
*/
setActive(active) {
this._active = active;
}
get propertyText() {
if (this.text !== undefined) {
return this.text;
}
if (this.name === '') {
return '';
}
return this.name + ': ' + this.value + (this.important ? ' !important' : '') + ';';
}
/**
* @return {boolean}
*/
activeInStyle() {
return this._active;
}
/**
* @param {string} propertyText
* @param {boolean} majorChange
* @param {boolean=} overwrite
* @return {!Promise.<boolean>}
*/
async setText(propertyText, majorChange, overwrite) {
if (!this.ownerStyle) {
return Promise.reject(new Error('No ownerStyle for property'));
}
if (!this.ownerStyle.styleSheetId) {
return Promise.reject(new Error('No owner style id'));
}
if (!this.range || !this.ownerStyle.range) {
return Promise.reject(new Error('Style not editable'));
}
if (majorChange) {
Host.userMetrics.actionTaken(Host.UserMetrics.Action.StyleRuleEdited);
}
if (overwrite && propertyText === this.propertyText) {
this.ownerStyle.cssModel().domModel().markUndoableState(!majorChange);
return Promise.resolve(true);
}
const range = this.range.relativeTo(this.ownerStyle.range.startLine, this.ownerStyle.range.startColumn);
const indentation = this.ownerStyle.cssText ? this._detectIndentation(this.ownerStyle.cssText) :
Common.moduleSetting('textEditorIndent').get();
const endIndentation = this.ownerStyle.cssText ? indentation.substring(0, this.ownerStyle.range.endColumn) : '';
const text = new TextUtils.Text(this.ownerStyle.cssText || '');
const newStyleText = text.replaceRange(range, String.sprintf(';%s;', propertyText));
const tokenizerFactory = await self.runtime.extension(TextUtils.TokenizerFactory).instance();
const styleText = CSSProperty._formatStyle(newStyleText, indentation, endIndentation, tokenizerFactory);
return this.ownerStyle.setText(styleText, majorChange);
}
/**
* @param {string} styleText
* @param {string} indentation
* @param {string} endIndentation
* @param {!TextUtils.TokenizerFactory} tokenizerFactory
* @return {string}
*/
static _formatStyle(styleText, indentation, endIndentation, tokenizerFactory) {
const doubleIndent = indentation.substring(endIndentation.length) + indentation;
if (indentation) {
indentation = '\n' + indentation;
}
let result = '';
let propertyName = '';
let propertyText;
let insideProperty = false;
let needsSemi = false;
const tokenize = tokenizerFactory.createTokenizer('text/css');
tokenize('*{' + styleText + '}', processToken);
if (insideProperty) {
result += propertyText;
}
result = result.substring(2, result.length - 1).trimRight();
return result + (indentation ? '\n' + endIndentation : '');
/**
* @param {string} token
* @param {?string} tokenType
* @param {number} column
* @param {number} newColumn
*/
function processToken(token, tokenType, column, newColumn) {
if (!insideProperty) {
const disabledProperty = tokenType && tokenType.includes('css-comment') && isDisabledProperty(token);
const isPropertyStart = tokenType &&
(tokenType.includes('css-string') || tokenType.includes('css-meta') || tokenType.includes('css-property') ||
tokenType.includes('css-variable-2'));
if (disabledProperty) {
result = result.trimRight() + indentation + token;
} else if (isPropertyStart) {
insideProperty = true;
propertyText = token;
} else if (token !== ';' || needsSemi) {
result += token;
if (token.trim() && !(tokenType && tokenType.includes('css-comment'))) {
needsSemi = token !== ';';
}
}
if (token === '{' && !tokenType) {
needsSemi = false;
}
return;
}
if (token === '}' || token === ';') {
result = result.trimRight() + indentation + propertyText.trim() + ';';
needsSemi = false;
insideProperty = false;
propertyName = '';
if (token === '}') {
result += '}';
}
} else {
if (SDK.cssMetadata().isGridAreaDefiningProperty(propertyName)) {
const rowResult = SDK.CSSMetadata.GridAreaRowRegex.exec(token);
if (rowResult && rowResult.index === 0 && !propertyText.trimRight().endsWith(']')) {
propertyText = propertyText.trimRight() + '\n' + doubleIndent;
}
}
if (!propertyName && token === ':') {
propertyName = propertyText;
}
propertyText += token;
}
}
/**
* @param {string} text
* @return {boolean}
*/
function isDisabledProperty(text) {
const colon = text.indexOf(':');
if (colon === -1) {
return false;
}
const propertyName = text.substring(2, colon).trim();
return SDK.cssMetadata().isCSSPropertyName(propertyName);
}
}
/**
* @param {string} text
* @return {string}
*/
_detectIndentation(text) {
const lines = text.split('\n');
if (lines.length < 2) {
return '';
}
return TextUtils.TextUtils.lineIndent(lines[1]);
}
/**
* @param {string} newValue
* @param {boolean} majorChange
* @param {boolean} overwrite
* @param {function(boolean)=} userCallback
*/
setValue(newValue, majorChange, overwrite, userCallback) {
const text = this.name + ': ' + newValue + (this.important ? ' !important' : '') + ';';
this.setText(text, majorChange, overwrite).then(userCallback);
}
/**
* @param {boolean} disabled
* @return {!Promise.<boolean>}
*/
setDisabled(disabled) {
if (!this.ownerStyle) {
return Promise.resolve(false);
}
if (disabled === this.disabled) {
return Promise.resolve(true);
}
const propertyText = this.text.trim();
const text = disabled ? '/* ' + propertyText + ' */' : this.text.substring(2, propertyText.length - 2).trim();
return this.setText(text, true, true);
}
}
/* Legacy exported object */
self.SDK = self.SDK || {};
/* Legacy exported object */
SDK = SDK || {};
/** @constructor */
SDK.CSSProperty = CSSProperty;