UNPKG

chrome-devtools-frontend

Version:
371 lines (329 loc) • 13.3 kB
// 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. import * as TextUtils from '../../models/text_utils/text_utils.js'; import type * as Protocol from '../../generated/protocol.js'; import {cssMetadata} from './CSSMetadata.js'; import {type CSSModel, type Edit} from './CSSModel.js'; import {CSSProperty} from './CSSProperty.js'; import {type CSSRule} from './CSSRule.js'; import {type Target} from './Target.js'; export class CSSStyleDeclaration { readonly #cssModelInternal: CSSModel; parentRule: CSSRule|null; #allPropertiesInternal!: CSSProperty[]; styleSheetId!: Protocol.CSS.StyleSheetId|undefined; range!: TextUtils.TextRange.TextRange|null; cssText!: string|undefined; #shorthandValues!: Map<string, string>; #shorthandIsImportant!: Set<string>; #activePropertyMap!: Map<string, CSSProperty>; #leadingPropertiesInternal!: CSSProperty[]|null; type: Type; constructor(cssModel: CSSModel, parentRule: CSSRule|null, payload: Protocol.CSS.CSSStyle, type: Type) { this.#cssModelInternal = cssModel; this.parentRule = parentRule; this.reinitialize(payload); this.type = type; } rebase(edit: Edit): void { if (this.styleSheetId !== edit.styleSheetId || !this.range) { return; } if (edit.oldRange.equal(this.range)) { this.reinitialize((edit.payload as Protocol.CSS.CSSStyle)); } else { this.range = this.range.rebaseAfterTextEdit(edit.oldRange, edit.newRange); for (let i = 0; i < this.#allPropertiesInternal.length; ++i) { this.#allPropertiesInternal[i].rebase(edit); } } } private reinitialize(payload: Protocol.CSS.CSSStyle): void { this.styleSheetId = payload.styleSheetId; this.range = payload.range ? TextUtils.TextRange.TextRange.fromObject(payload.range) : null; const shorthandEntries = payload.shorthandEntries; this.#shorthandValues = new Map(); this.#shorthandIsImportant = new Set(); for (let i = 0; i < shorthandEntries.length; ++i) { this.#shorthandValues.set(shorthandEntries[i].name, shorthandEntries[i].value); if (shorthandEntries[i].important) { this.#shorthandIsImportant.add(shorthandEntries[i].name); } } this.#allPropertiesInternal = []; if (payload.cssText && this.range) { const cssText = new TextUtils.Text.Text(payload.cssText); let start: { line: number, column: number, }|{ line: number, column: number, } = {line: this.range.startLine, column: this.range.startColumn}; for (const cssProperty of payload.cssProperties) { const range = cssProperty.range; if (range) { parseUnusedText.call(this, cssText, start.line, start.column, range.startLine, range.startColumn); start = {line: range.endLine, column: range.endColumn}; } this.#allPropertiesInternal.push( CSSProperty.parsePayload(this, this.#allPropertiesInternal.length, cssProperty)); } parseUnusedText.call(this, cssText, start.line, start.column, this.range.endLine, this.range.endColumn); } else { for (const cssProperty of payload.cssProperties) { this.#allPropertiesInternal.push( CSSProperty.parsePayload(this, this.#allPropertiesInternal.length, cssProperty)); } } this.generateSyntheticPropertiesIfNeeded(); this.computeInactiveProperties(); this.#activePropertyMap = new Map(); for (const property of this.#allPropertiesInternal) { if (!property.activeInStyle()) { continue; } this.#activePropertyMap.set(property.name, property); } this.cssText = payload.cssText; this.#leadingPropertiesInternal = null; function parseUnusedText( this: CSSStyleDeclaration, cssText: TextUtils.Text.Text, startLine: number, startColumn: number, endLine: number, endColumn: number): void { const tr = new TextUtils.TextRange.TextRange(startLine, startColumn, endLine, endColumn); if (!this.range) { return; } const missingText = cssText.extract(tr.relativeTo(this.range.startLine, this.range.startColumn)); // Try to fit the malformed css into properties. const lines = missingText.split('\n'); let lineNumber = 0; let inComment = false; for (const line of lines) { let column = 0; for (const property of line.split(';')) { const strippedProperty = stripComments(property, inComment); const trimmedProperty = strippedProperty.text.trim(); inComment = strippedProperty.inComment; if (trimmedProperty) { let name; let value; const colonIndex = trimmedProperty.indexOf(':'); if (colonIndex === -1) { name = trimmedProperty; value = ''; } else { name = trimmedProperty.substring(0, colonIndex).trim(); value = trimmedProperty.substring(colonIndex + 1).trim(); } const range = new TextUtils.TextRange.TextRange(lineNumber, column, lineNumber, column + property.length); this.#allPropertiesInternal.push(new CSSProperty( this, this.#allPropertiesInternal.length, name, value, false, false, false, false, property, range.relativeFrom(startLine, startColumn))); } column += property.length + 1; } lineNumber++; } } function stripComments(text: string, inComment: boolean): { text: string, inComment: boolean, } { let output = ''; for (let i = 0; i < text.length; i++) { if (!inComment && text.substring(i, i + 2) === '/*') { inComment = true; i++; } else if (inComment && text.substring(i, i + 2) === '*/') { inComment = false; i++; } else if (!inComment) { output += text[i]; } } return {text: output, inComment}; } } private generateSyntheticPropertiesIfNeeded(): void { if (this.range) { return; } if (!this.#shorthandValues.size) { return; } const propertiesSet = new Set<string>(); for (const property of this.#allPropertiesInternal) { propertiesSet.add(property.name); } const generatedProperties = []; // For style-based properties, generate #shorthands with values when possible. for (const property of this.#allPropertiesInternal) { // For style-based properties, try generating #shorthands. const shorthands = cssMetadata().getShorthands(property.name) || []; for (const shorthand of shorthands) { if (propertiesSet.has(shorthand)) { continue; } // There already is a shorthand this #longhand falls under. const shorthandValue = this.#shorthandValues.get(shorthand); if (!shorthandValue) { continue; } // Never generate synthetic #shorthands when no value is available. // Generate synthetic shorthand we have a value for. const shorthandImportance = Boolean(this.#shorthandIsImportant.has(shorthand)); const shorthandProperty = new CSSProperty( this, this.allProperties().length, shorthand, shorthandValue, shorthandImportance, false, true, false); generatedProperties.push(shorthandProperty); propertiesSet.add(shorthand); } } this.#allPropertiesInternal = this.#allPropertiesInternal.concat(generatedProperties); } private computeLeadingProperties(): CSSProperty[] { function propertyHasRange(property: CSSProperty): boolean { return Boolean(property.range); } if (this.range) { return this.#allPropertiesInternal.filter(propertyHasRange); } const leadingProperties = []; for (const property of this.#allPropertiesInternal) { const shorthands = cssMetadata().getShorthands(property.name) || []; let belongToAnyShorthand = false; for (const shorthand of shorthands) { if (this.#shorthandValues.get(shorthand)) { belongToAnyShorthand = true; break; } } if (!belongToAnyShorthand) { leadingProperties.push(property); } } return leadingProperties; } leadingProperties(): CSSProperty[] { if (!this.#leadingPropertiesInternal) { this.#leadingPropertiesInternal = this.computeLeadingProperties(); } return this.#leadingPropertiesInternal; } target(): Target { return this.#cssModelInternal.target(); } cssModel(): CSSModel { return this.#cssModelInternal; } private computeInactiveProperties(): void { const activeProperties = new Map<string, CSSProperty>(); for (let i = 0; i < this.#allPropertiesInternal.length; ++i) { const property = this.#allPropertiesInternal[i]; if (property.disabled || !property.parsedOk) { property.setActive(false); continue; } const metadata = cssMetadata(); const canonicalName = metadata.canonicalPropertyName(property.name); const longhands = metadata.getLonghands(canonicalName); if (longhands) { for (const longhand of longhands) { const activeLonghand = activeProperties.get(longhand); if (activeLonghand && activeLonghand.range && (!activeLonghand.important || property.important)) { activeLonghand.setActive(false); activeProperties.delete(longhand); } } } const activeProperty = activeProperties.get(canonicalName); if (!activeProperty) { activeProperties.set(canonicalName, property); } else if (!property.range) { // For some -webkit- properties, the backend returns also the canonical // property. e.g. if you set in the css only the property // -webkit-background-clip, the backend will return // -webkit-background-clip and background-clip. // This behavior will invalidate -webkit-background-clip (only visually, // the property will be correctly applied) // So this is checking if the property is visible or not in the // styles panel and if not, it will not deactivate the "activeProperty". property.setActive(false); } else if (!activeProperty.important || property.important) { activeProperty.setActive(false); activeProperties.set(canonicalName, property); } else { property.setActive(false); } } } allProperties(): CSSProperty[] { return this.#allPropertiesInternal; } hasActiveProperty(name: string): boolean { return this.#activePropertyMap.has(name); } getPropertyValue(name: string): string { const property = this.#activePropertyMap.get(name); return property ? property.value : ''; } isPropertyImplicit(name: string): boolean { const property = this.#activePropertyMap.get(name); return property ? property.implicit : false; } longhandProperties(name: string): CSSProperty[] { const longhands = cssMetadata().getLonghands(name.toLowerCase()); const result = []; for (let i = 0; longhands && i < longhands.length; ++i) { const property = this.#activePropertyMap.get(longhands[i]); if (property) { result.push(property); } } return result; } propertyAt(index: number): CSSProperty|null { return (index < this.allProperties().length) ? this.allProperties()[index] : null; } pastLastSourcePropertyIndex(): number { for (let i = this.allProperties().length - 1; i >= 0; --i) { if (this.allProperties()[i].range) { return i + 1; } } return 0; } private insertionRange(index: number): TextUtils.TextRange.TextRange { const property = this.propertyAt(index); if (property && property.range) { return property.range.collapseToStart(); } if (!this.range) { throw new Error('CSSStyleDeclaration.range is null'); } return this.range.collapseToEnd(); } newBlankProperty(index?: number): CSSProperty { index = (typeof index === 'undefined') ? this.pastLastSourcePropertyIndex() : index; const property = new CSSProperty(this, index, '', '', false, false, true, false, '', this.insertionRange(index)); return property; } setText(text: string, majorChange: boolean): Promise<boolean> { if (!this.range || !this.styleSheetId) { return Promise.resolve(false); } return this.#cssModelInternal.setStyleText(this.styleSheetId, this.range, text, majorChange); } insertPropertyAt(index: number, name: string, value: string, userCallback?: ((arg0: boolean) => void)): void { void this.newBlankProperty(index).setText(name + ': ' + value + ';', false, true).then(userCallback); } appendProperty(name: string, value: string, userCallback?: ((arg0: boolean) => void)): void { this.insertPropertyAt(this.allProperties().length, name, value, userCallback); } } // TODO(crbug.com/1167717): Make this a const enum again // eslint-disable-next-line rulesdir/const_enum export enum Type { Regular = 'Regular', Inline = 'Inline', Attributes = 'Attributes', }