/*--------------------------------------------------------------------------------------------- * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ 'use strict'; import * as nodes from '../parser/cssNodes'; import { Scanner } from '../parser/cssScanner'; import * as l10n from '@vscode/l10n'; export class Element { constructor() { this.parent = null; this.children = null; this.attributes = null; } findAttribute(name) { if (this.attributes) { for (const attribute of this.attributes) { if (attribute.name === name) { return attribute.value; } } } return null; } addChild(child) { if (child instanceof Element) { child.parent = this; } if (!this.children) { this.children = []; } this.children.push(child); } append(text) { if (this.attributes) { const last = this.attributes[this.attributes.length - 1]; last.value = last.value + text; } } prepend(text) { if (this.attributes) { const first = this.attributes[0]; first.value = text + first.value; } } findRoot() { let curr = this; while (curr.parent && !(curr.parent instanceof RootElement)) { curr = curr.parent; } return curr; } removeChild(child) { if (this.children) { const index = this.children.indexOf(child); if (index !== -1) { this.children.splice(index, 1); return true; } } return false; } addAttr(name, value) { if (!this.attributes) { this.attributes = []; } for (const attribute of this.attributes) { if (attribute.name === name) { attribute.value += ' ' + value; return; } } this.attributes.push({ name, value }); } clone(cloneChildren = true) { const elem = new Element(); if (this.attributes) { elem.attributes = []; for (const attribute of this.attributes) { elem.addAttr(attribute.name, attribute.value); } } if (cloneChildren && this.children) { elem.children = []; for (let index = 0; index < this.children.length; index++) { elem.addChild(this.children[index].clone()); } } return elem; } cloneWithParent() { const clone = this.clone(false); if (this.parent && !(this.parent instanceof RootElement)) { const parentClone = this.parent.cloneWithParent(); parentClone.addChild(clone); } return clone; } } export class RootElement extends Element { } export class LabelElement extends Element { constructor(label) { super(); this.addAttr('name', label); } } class MarkedStringPrinter { constructor(quote) { this.quote = quote; this.result = []; // empty } print(element) { this.result = []; if (element instanceof RootElement) { if (element.children) { this.doPrint(element.children, 0); } } else { this.doPrint([element], 0); } const value = this.result.join('\n'); return [{ language: 'html', value }]; } doPrint(elements, indent) { for (const element of elements) { this.doPrintElement(element, indent); if (element.children) { this.doPrint(element.children, indent + 1); } } } writeLine(level, content) { const indent = new Array(level + 1).join(' '); this.result.push(indent + content); } doPrintElement(element, indent) { const name = element.findAttribute('name'); // special case: a simple label if (element instanceof LabelElement || name === '\u2026') { this.writeLine(indent, name); return; } // the real deal const content = ['<']; // element name if (name) { content.push(name); } else { content.push('element'); } // attributes if (element.attributes) { for (const attr of element.attributes) { if (attr.name !== 'name') { content.push(' '); content.push(attr.name); const value = attr.value; if (value) { content.push('='); content.push(quotes.ensure(value, this.quote)); } } } } content.push('>'); this.writeLine(indent, content.join('')); } } var quotes; (function (quotes) { function ensure(value, which) { return which + remove(value) + which; } quotes.ensure = ensure; function remove(value) { const match = value.match(/^['"](.*)["']$/); if (match) { return match[1]; } return value; } quotes.remove = remove; })(quotes || (quotes = {})); class Specificity { constructor() { /** Count of identifiers (e.g., `#app`) */ this.id = 0; /** Count of attributes (`[type="number"]`), classes (`.container-fluid`), and pseudo-classes (`:hover`) */ this.attr = 0; /** Count of tag names (`div`), and pseudo-elements (`::before`) */ this.tag = 0; } } export function toElement(node, parentElement) { let result = new Element(); for (const child of node.getChildren()) { switch (child.type) { case nodes.NodeType.SelectorCombinator: if (parentElement) { const segments = child.getText().split('&'); if (segments.length === 1) { // should not happen result.addAttr('name', segments[0]); break; } result = parentElement.cloneWithParent(); if (segments[0]) { const root = result.findRoot(); root.prepend(segments[0]); } for (let i = 1; i < segments.length; i++) { if (i > 1) { const clone = parentElement.cloneWithParent(); result.addChild(clone.findRoot()); result = clone; } result.append(segments[i]); } } break; case nodes.NodeType.SelectorPlaceholder: if (child.matches('@at-root')) { return result; } // fall through case nodes.NodeType.ElementNameSelector: const text = child.getText(); result.addAttr('name', text === '*' ? 'element' : unescape(text)); break; case nodes.NodeType.ClassSelector: result.addAttr('class', unescape(child.getText().substring(1))); break; case nodes.NodeType.IdentifierSelector: result.addAttr('id', unescape(child.getText().substring(1))); break; case nodes.NodeType.MixinDeclaration: result.addAttr('class', child.getName()); break; case nodes.NodeType.PseudoSelector: result.addAttr(unescape(child.getText()), ''); break; case nodes.NodeType.AttributeSelector: const selector = child; const identifier = selector.getIdentifier(); if (identifier) { const expression = selector.getValue(); const operator = selector.getOperator(); let value; if (expression && operator) { switch (unescape(operator.getText())) { case '|=': // excatly or followed by -words value = `${quotes.remove(unescape(expression.getText()))}-\u2026`; break; case '^=': // prefix value = `${quotes.remove(unescape(expression.getText()))}\u2026`; break; case '$=': // suffix value = `\u2026${quotes.remove(unescape(expression.getText()))}`; break; case '~=': // one of a list of words value = ` \u2026 ${quotes.remove(unescape(expression.getText()))} \u2026 `; break; case '*=': // substring value = `\u2026${quotes.remove(unescape(expression.getText()))}\u2026`; break; default: value = quotes.remove(unescape(expression.getText())); break; } } result.addAttr(unescape(identifier.getText()), value); } break; } } return result; } function unescape(content) { const scanner = new Scanner(); scanner.setSource(content); const token = scanner.scanUnquotedString(); if (token) { return token.text; } return content; } export class SelectorPrinting { constructor(cssDataManager) { this.cssDataManager = cssDataManager; } selectorToMarkedString(node) { const root = selectorToElement(node); if (root) { const markedStrings = new MarkedStringPrinter('"').print(root); markedStrings.push(this.selectorToSpecificityMarkedString(node)); return markedStrings; } else { return []; } } simpleSelectorToMarkedString(node) { const element = toElement(node); const markedStrings = new MarkedStringPrinter('"').print(element); markedStrings.push(this.selectorToSpecificityMarkedString(node)); return markedStrings; } isPseudoElementIdentifier(text) { const match = text.match(/^::?([\w-]+)/); if (!match) { return false; } return !!this.cssDataManager.getPseudoElement("::" + match[1]); } selectorToSpecificityMarkedString(node) { //https://www.w3.org/TR/selectors-3/#specificity const calculateScore = (node) => { const specificity = new Specificity(); elementLoop: for (const element of node.getChildren()) { switch (element.type) { case nodes.NodeType.IdentifierSelector: specificity.id++; break; case nodes.NodeType.ClassSelector: case nodes.NodeType.AttributeSelector: specificity.attr++; break; case nodes.NodeType.ElementNameSelector: //ignore universal selector if (element.matches("*")) { break; } specificity.tag++; break; case nodes.NodeType.PseudoSelector: const text = element.getText(); if (this.isPseudoElementIdentifier(text)) { specificity.tag++; // pseudo element continue elementLoop; } // where and child selectors have zero specificity if (text.match(/^:where/i)) { continue elementLoop; } // the most specific child selector if (text.match(/^:(not|has|is)/i) && element.getChildren().length > 0) { let mostSpecificListItem = new Specificity(); for (const containerElement of element.getChildren()) { let list; if (containerElement.type === nodes.NodeType.Undefined) { // containerElement is a list of selectors list = containerElement.getChildren(); } else { // containerElement is a selector list = [containerElement]; } for (const childElement of containerElement.getChildren()) { const itemSpecificity = calculateScore(childElement); if (itemSpecificity.id > mostSpecificListItem.id) { mostSpecificListItem = itemSpecificity; continue; } else if (itemSpecificity.id < mostSpecificListItem.id) { continue; } if (itemSpecificity.attr > mostSpecificListItem.attr) { mostSpecificListItem = itemSpecificity; continue; } else if (itemSpecificity.attr < mostSpecificListItem.attr) { continue; } if (itemSpecificity.tag > mostSpecificListItem.tag) { mostSpecificListItem = itemSpecificity; continue; } } } specificity.id += mostSpecificListItem.id; specificity.attr += mostSpecificListItem.attr; specificity.tag += mostSpecificListItem.tag; continue elementLoop; } specificity.attr++; //pseudo class continue elementLoop; } if (element.getChildren().length > 0) { const itemSpecificity = calculateScore(element); specificity.id += itemSpecificity.id; specificity.attr += itemSpecificity.attr; specificity.tag += itemSpecificity.tag; } } return specificity; }; const specificity = calculateScore(node); return `[${l10n.t("Selector Specificity")}](https://developer.mozilla.org/docs/Web/CSS/Specificity): (${specificity.id}, ${specificity.attr}, ${specificity.tag})`; } } class SelectorElementBuilder { constructor(element) { this.prev = null; this.element = element; } processSelector(selector) { let parentElement = null; if (!(this.element instanceof RootElement)) { if (selector.getChildren().some((c) => c.hasChildren() && c.getChild(0).type === nodes.NodeType.SelectorCombinator)) { const curr = this.element.findRoot(); if (curr.parent instanceof RootElement) { parentElement = this.element; this.element = curr.parent; this.element.removeChild(curr); this.prev = null; } } } for (const selectorChild of selector.getChildren()) { if (selectorChild instanceof nodes.SimpleSelector) { if (this.prev instanceof nodes.SimpleSelector) { const labelElement = new LabelElement('\u2026'); this.element.addChild(labelElement); this.element = labelElement; } else if (this.prev && (this.prev.matches('+') || this.prev.matches('~')) && this.element.parent) { this.element = this.element.parent; } if (this.prev && this.prev.matches('~')) { this.element.addChild(new LabelElement('\u22EE')); } const thisElement = toElement(selectorChild, parentElement); const root = thisElement.findRoot(); this.element.addChild(root); this.element = thisElement; } if (selectorChild instanceof nodes.SimpleSelector || selectorChild.type === nodes.NodeType.SelectorCombinatorParent || selectorChild.type === nodes.NodeType.SelectorCombinatorShadowPiercingDescendant || selectorChild.type === nodes.NodeType.SelectorCombinatorSibling || selectorChild.type === nodes.NodeType.SelectorCombinatorAllSiblings) { this.prev = selectorChild; } } } } function isNewSelectorContext(node) { switch (node.type) { case nodes.NodeType.MixinDeclaration: case nodes.NodeType.Stylesheet: return true; } return false; } export function selectorToElement(node) { if (node.matches('@at-root')) { return null; } const root = new RootElement(); const parentRuleSets = []; const ruleSet = node.getParent(); if (ruleSet instanceof nodes.RuleSet) { let parent = ruleSet.getParent(); // parent of the selector's ruleset while (parent && !isNewSelectorContext(parent)) { if (parent instanceof nodes.RuleSet) { if (parent.getSelectors().matches('@at-root')) { break; } parentRuleSets.push(parent); } parent = parent.getParent(); } } const builder = new SelectorElementBuilder(root); for (let i = parentRuleSets.length - 1; i >= 0; i--) { const selector = parentRuleSets[i].getSelectors().getChild(0); if (selector) { builder.processSelector(selector); } } builder.processSelector(node); return root; }