From a1a5c2eed6a638e1fdb4cddd0d102f84863603c6 Mon Sep 17 00:00:00 2001 From: balaji-sivasakthi Date: Thu, 22 Feb 2024 22:37:33 +0530 Subject: [PATCH] fix: `toString()` and code style --- src/services/cssHover.ts | 322 ++++---- src/services/selectorPrinting.ts | 1184 +++++++++++++++--------------- src/test/css/hover.test.ts | 167 +++-- 3 files changed, 831 insertions(+), 842 deletions(-) diff --git a/src/services/cssHover.ts b/src/services/cssHover.ts index c4a3eae6..e14e4bf9 100644 --- a/src/services/cssHover.ts +++ b/src/services/cssHover.ts @@ -13,166 +13,164 @@ import { isDefined } from '../utils/objects'; import { CSSDataManager } from '../languageFacts/dataManager'; export class CSSHover { - private supportsMarkdown: boolean | undefined; - private readonly selectorPrinting: SelectorPrinting; - private defaultSettings?: HoverSettings; - - constructor(private readonly clientCapabilities: ClientCapabilities | undefined, private readonly cssDataManager: CSSDataManager) { - this.selectorPrinting = new SelectorPrinting(cssDataManager); - } - - public configure(settings: HoverSettings | undefined) { - this.defaultSettings = settings; - } - - - public doHover(document: TextDocument, position: Position, stylesheet: nodes.Stylesheet, settings = this.defaultSettings): Hover | null { - function getRange(node: nodes.Node) { - return Range.create(document.positionAt(node.offset), document.positionAt(node.end)); - } - const offset = document.offsetAt(position); - const nodepath = nodes.getNodePath(stylesheet, offset); - - /** - * nodepath is top-down - * Build up the hover by appending inner node's information - */ - let hover: Hover | null = null; - let flagOpts:{text:string;isMedia:boolean}; - - for (let i = 0; i < nodepath.length; i++) { - const node = nodepath[i]; - - if (node instanceof nodes.Media){ - const regex = /@media[^\{]+/g; - const matches = node.getText().match(regex); - flagOpts = { - isMedia:true, - text:matches?.[0].toString()! - }; - } - - if (node instanceof nodes.Selector) { - hover = { - contents: this.selectorPrinting.selectorToMarkedString(node, flagOpts!), - range: getRange(node) - }; - break; - } - - if (node instanceof nodes.SimpleSelector) { - /** - * Some sass specific at rules such as `@at-root` are parsed as `SimpleSelector` - */ - if (!startsWith(node.getText(), '@')) { - hover = { - contents: this.selectorPrinting.simpleSelectorToMarkedString(node), - range: getRange(node) - }; - } - break; - } - - if (node instanceof nodes.Declaration) { - const propertyName = node.getFullPropertyName(); - const entry = this.cssDataManager.getProperty(propertyName); - if (entry) { - const contents = languageFacts.getEntryDescription(entry, this.doesSupportMarkdown(), settings); - if (contents) { - hover = { - contents, - range: getRange(node) - }; - } else { - hover = null; - } - } - continue; - } - - if (node instanceof nodes.UnknownAtRule) { - const atRuleName = node.getText(); - const entry = this.cssDataManager.getAtDirective(atRuleName); - if (entry) { - const contents = languageFacts.getEntryDescription(entry, this.doesSupportMarkdown(), settings); - if (contents) { - hover = { - contents, - range: getRange(node) - }; - } else { - hover = null; - } - } - continue; - } - - if (node instanceof nodes.Node && node.type === nodes.NodeType.PseudoSelector) { - const selectorName = node.getText(); - const entry = - selectorName.slice(0, 2) === '::' - ? this.cssDataManager.getPseudoElement(selectorName) - : this.cssDataManager.getPseudoClass(selectorName); - if (entry) { - const contents = languageFacts.getEntryDescription(entry, this.doesSupportMarkdown(), settings); - if (contents) { - hover = { - contents, - range: getRange(node) - }; - } else { - hover = null; - } - } - continue; - } - } - - - if (hover) { - hover.contents = this.convertContents(hover.contents); - } - - return hover; - } - - private convertContents(contents: MarkupContent | MarkedString | MarkedString[]): MarkupContent | MarkedString | MarkedString[] { - if (!this.doesSupportMarkdown()) { - if (typeof contents === 'string') { - return contents; - } - // MarkupContent - else if ('kind' in contents) { - return { - kind: 'plaintext', - value: contents.value - }; - } - // MarkedString[] - else if (Array.isArray(contents)) { - return contents.map(c => { - return typeof c === 'string' ? c : c.value; - }); - } - // MarkedString - else { - return contents.value; - } - } - - return contents; - } - - private doesSupportMarkdown() { - if (!isDefined(this.supportsMarkdown)) { - if (!isDefined(this.clientCapabilities)) { - this.supportsMarkdown = true; - return this.supportsMarkdown; - } - - const hover = this.clientCapabilities.textDocument && this.clientCapabilities.textDocument.hover; - this.supportsMarkdown = hover && hover.contentFormat && Array.isArray(hover.contentFormat) && hover.contentFormat.indexOf(MarkupKind.Markdown) !== -1; - } - return this.supportsMarkdown; - } + private supportsMarkdown: boolean | undefined; + private readonly selectorPrinting: SelectorPrinting; + private defaultSettings?: HoverSettings; + + constructor( + private readonly clientCapabilities: ClientCapabilities | undefined, + private readonly cssDataManager: CSSDataManager, + ) { + this.selectorPrinting = new SelectorPrinting(cssDataManager); + } + + public configure(settings: HoverSettings | undefined) { + this.defaultSettings = settings; + } + + public doHover(document: TextDocument, position: Position, stylesheet: nodes.Stylesheet, settings = this.defaultSettings): Hover | null { + function getRange(node: nodes.Node) { + return Range.create(document.positionAt(node.offset), document.positionAt(node.end)); + } + const offset = document.offsetAt(position); + const nodepath = nodes.getNodePath(stylesheet, offset); + + /** + * nodepath is top-down + * Build up the hover by appending inner node's information + */ + let hover: Hover | null = null; + let flagOpts: { text: string; isMedia: boolean }; + + for (let i = 0; i < nodepath.length; i++) { + const node = nodepath[i]; + + if (node instanceof nodes.Media) { + const regex = /@media[^\{]+/g; + const matches = node.getText().match(regex); + flagOpts = { + isMedia: true, + text: matches?.[0]!, + }; + } + + if (node instanceof nodes.Selector) { + hover = { + contents: this.selectorPrinting.selectorToMarkedString(node, flagOpts!), + range: getRange(node), + }; + break; + } + + if (node instanceof nodes.SimpleSelector) { + /** + * Some sass specific at rules such as `@at-root` are parsed as `SimpleSelector` + */ + if (!startsWith(node.getText(), '@')) { + hover = { + contents: this.selectorPrinting.simpleSelectorToMarkedString(node), + range: getRange(node), + }; + } + break; + } + + if (node instanceof nodes.Declaration) { + const propertyName = node.getFullPropertyName(); + const entry = this.cssDataManager.getProperty(propertyName); + if (entry) { + const contents = languageFacts.getEntryDescription(entry, this.doesSupportMarkdown(), settings); + if (contents) { + hover = { + contents, + range: getRange(node), + }; + } else { + hover = null; + } + } + continue; + } + + if (node instanceof nodes.UnknownAtRule) { + const atRuleName = node.getText(); + const entry = this.cssDataManager.getAtDirective(atRuleName); + if (entry) { + const contents = languageFacts.getEntryDescription(entry, this.doesSupportMarkdown(), settings); + if (contents) { + hover = { + contents, + range: getRange(node), + }; + } else { + hover = null; + } + } + continue; + } + + if (node instanceof nodes.Node && node.type === nodes.NodeType.PseudoSelector) { + const selectorName = node.getText(); + const entry = selectorName.slice(0, 2) === '::' ? this.cssDataManager.getPseudoElement(selectorName) : this.cssDataManager.getPseudoClass(selectorName); + if (entry) { + const contents = languageFacts.getEntryDescription(entry, this.doesSupportMarkdown(), settings); + if (contents) { + hover = { + contents, + range: getRange(node), + }; + } else { + hover = null; + } + } + continue; + } + } + + if (hover) { + hover.contents = this.convertContents(hover.contents); + } + + return hover; + } + + private convertContents(contents: MarkupContent | MarkedString | MarkedString[]): MarkupContent | MarkedString | MarkedString[] { + if (!this.doesSupportMarkdown()) { + if (typeof contents === 'string') { + return contents; + } + // MarkupContent + else if ('kind' in contents) { + return { + kind: 'plaintext', + value: contents.value, + }; + } + // MarkedString[] + else if (Array.isArray(contents)) { + return contents.map((c) => { + return typeof c === 'string' ? c : c.value; + }); + } + // MarkedString + else { + return contents.value; + } + } + + return contents; + } + + private doesSupportMarkdown() { + if (!isDefined(this.supportsMarkdown)) { + if (!isDefined(this.clientCapabilities)) { + this.supportsMarkdown = true; + return this.supportsMarkdown; + } + + const hover = this.clientCapabilities.textDocument && this.clientCapabilities.textDocument.hover; + this.supportsMarkdown = hover && hover.contentFormat && Array.isArray(hover.contentFormat) && hover.contentFormat.indexOf(MarkupKind.Markdown) !== -1; + } + return this.supportsMarkdown; + } } diff --git a/src/services/selectorPrinting.ts b/src/services/selectorPrinting.ts index e39332d0..e53a2997 100644 --- a/src/services/selectorPrinting.ts +++ b/src/services/selectorPrinting.ts @@ -12,634 +12,620 @@ import { CSSDataManager } from '../languageFacts/dataManager'; import { Parser } from '../parser/cssParser'; export class Element { - - public parent: Element | null = null; - public children: Element[] | null = null; - public attributes: { name: string, value: string; }[] | null = null; - - public findAttribute(name: string): string | null { - if (this.attributes) { - for (const attribute of this.attributes) { - if (attribute.name === name) { - return attribute.value; - } - } - } - return null; - } - - public addChild(child: Element): void { - if (child instanceof Element) { - (child).parent = this; - } - if (!this.children) { - this.children = []; - } - this.children.push(child); - } - - public append(text: string) { - if (this.attributes) { - const last = this.attributes[this.attributes.length - 1]; - last.value = last.value + text; - } - } - - public prepend(text: string) { - if (this.attributes) { - const first = this.attributes[0]; - first.value = text + first.value; - } - } - - public findRoot(): Element { - let curr: Element = this; - while (curr.parent && !(curr.parent instanceof RootElement)) { - curr = curr.parent; - } - return curr; - } - - public removeChild(child: Element): boolean { - if (this.children) { - const index = this.children.indexOf(child); - if (index !== -1) { - this.children.splice(index, 1); - return true; - } - } - return false; - } - - public addAttr(name: string, value: string): void { - if (!this.attributes) { - this.attributes = []; - } - for (const attribute of this.attributes) { - if (attribute.name === name) { - attribute.value += ' ' + value; - return; - } - } - this.attributes.push({ name, value }); - } - - public clone(cloneChildren: boolean = true): Element { - 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; - } - - public cloneWithParent(): Element { - const clone = this.clone(false); - if (this.parent && !(this.parent instanceof RootElement)) { - const parentClone = this.parent.cloneWithParent(); - parentClone.addChild(clone); - } - return clone; - } + public parent: Element | null = null; + public children: Element[] | null = null; + public attributes: { name: string; value: string }[] | null = null; + + public findAttribute(name: string): string | null { + if (this.attributes) { + for (const attribute of this.attributes) { + if (attribute.name === name) { + return attribute.value; + } + } + } + return null; + } + + public addChild(child: Element): void { + if (child instanceof Element) { + (child).parent = this; + } + if (!this.children) { + this.children = []; + } + this.children.push(child); + } + + public append(text: string) { + if (this.attributes) { + const last = this.attributes[this.attributes.length - 1]; + last.value = last.value + text; + } + } + + public prepend(text: string) { + if (this.attributes) { + const first = this.attributes[0]; + first.value = text + first.value; + } + } + + public findRoot(): Element { + let curr: Element = this; + while (curr.parent && !(curr.parent instanceof RootElement)) { + curr = curr.parent; + } + return curr; + } + + public removeChild(child: Element): boolean { + if (this.children) { + const index = this.children.indexOf(child); + if (index !== -1) { + this.children.splice(index, 1); + return true; + } + } + return false; + } + + public addAttr(name: string, value: string): void { + if (!this.attributes) { + this.attributes = []; + } + for (const attribute of this.attributes) { + if (attribute.name === name) { + attribute.value += ' ' + value; + return; + } + } + this.attributes.push({ name, value }); + } + + public clone(cloneChildren: boolean = true): Element { + 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; + } + + public cloneWithParent(): Element { + 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 RootElement extends Element {} export class LabelElement extends Element { - - constructor(label: string) { - super(); - this.addAttr('name', label); - } + constructor(label: string) { + super(); + this.addAttr('name', label); + } } class MarkedStringPrinter { - - private result: string[] = []; - - constructor(public quote: string) { - // empty - } - - public print(element: Element, flagOpts?:{isMedia: boolean, text: string}): MarkedString[] { - this.result = []; - if (element instanceof RootElement) { - if (element.children) { - this.doPrint(element.children, 0); - } - } else { - this.doPrint([element], 0); - } - let value ; - if(flagOpts){ - value = `${flagOpts.text}\n … ` + this.result.join('\n'); - }else{ - value = this.result.join('\n'); - } - return [{ language: 'html', value }]; - } - - private doPrint(elements: Element[], indent: number) { - for (const element of elements) { - this.doPrintElement(element, indent); - if (element.children) { - this.doPrint(element.children, indent + 1); - } - } - } - - private writeLine(level: number, content: string) { - const indent = new Array(level + 1).join(' '); - this.result.push(indent + content); - } - - private doPrintElement(element: Element, indent: number) { - 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('')); - } + private result: string[] = []; + + constructor(public quote: string) { + // empty + } + + public print(element: Element, flagOpts?: { isMedia: boolean; text: string }): MarkedString[] { + this.result = []; + if (element instanceof RootElement) { + if (element.children) { + this.doPrint(element.children, 0); + } + } else { + this.doPrint([element], 0); + } + let value; + if (flagOpts) { + value = `${flagOpts.text}\n … ` + this.result.join('\n'); + } else { + value = this.result.join('\n'); + } + return [{ language: 'html', value }]; + } + + private doPrint(elements: Element[], indent: number) { + for (const element of elements) { + this.doPrintElement(element, indent); + if (element.children) { + this.doPrint(element.children, indent + 1); + } + } + } + + private writeLine(level: number, content: string) { + const indent = new Array(level + 1).join(' '); + this.result.push(indent + content); + } + + private doPrintElement(element: Element, indent: number) { + 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('')); + } } - namespace quotes { - - export function ensure(value: string, which: string): string { - return which + remove(value) + which; - } - - export function remove(value: string): string { - const match = value.match(/^['"](.*)["']$/); - if (match) { - return match[1]; - } - return value; - } + export function ensure(value: string, which: string): string { + return which + remove(value) + which; + } + + export function remove(value: string): string { + const match = value.match(/^['"](.*)["']$/); + if (match) { + return match[1]; + } + return value; + } } class Specificity { - /** Count of identifiers (e.g., `#app`) */ - public id = 0; - /** Count of attributes (`[type="number"]`), classes (`.container-fluid`), and pseudo-classes (`:hover`) */ - public attr = 0; - /** Count of tag names (`div`), and pseudo-elements (`::before`) */ - public tag = 0; + /** Count of identifiers (e.g., `#app`) */ + public id = 0; + /** Count of attributes (`[type="number"]`), classes (`.container-fluid`), and pseudo-classes (`:hover`) */ + public attr = 0; + /** Count of tag names (`div`), and pseudo-elements (`::before`) */ + public tag = 0; } export function toElement(node: nodes.SimpleSelector, parentElement?: Element | null): Element { - - 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: string; - 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; + 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: string; + 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: string) { - const scanner = new Scanner(); - scanner.setSource(content); - const token = scanner.scanUnquotedString(); - if (token) { - return token.text; - } - return content; + const scanner = new Scanner(); + scanner.setSource(content); + const token = scanner.scanUnquotedString(); + if (token) { + return token.text; + } + return content; } - export class SelectorPrinting { - - constructor(private cssDataManager: CSSDataManager) { - - } - - public selectorToMarkedString(node: nodes.Selector, flagOpts?:{isMedia: boolean, text: string}): MarkedString[] { - const root = selectorToElement(node); - if (root) { - const markedStrings = new MarkedStringPrinter('"').print(root, flagOpts); - markedStrings.push(this.selectorToSpecificityMarkedString(node)); - return markedStrings; - } else { - return []; - } - } - - public simpleSelectorToMarkedString(node: nodes.SimpleSelector): MarkedString[] { - const element = toElement(node); - const markedStrings = new MarkedStringPrinter('"').print(element); - markedStrings.push(this.selectorToSpecificityMarkedString(node)); - return markedStrings; - } - - private isPseudoElementIdentifier(text: string): boolean { - const match = text.match(/^::?([\w-]+)/); - - if (!match) { - return false; - } - - return !!this.cssDataManager.getPseudoElement("::" + match[1]); - } - - private selectorToSpecificityMarkedString(node: nodes.Node): MarkedString { - const calculateMostSpecificListItem = (childElements: Array): Specificity => { - const specificity = new Specificity(); - - let mostSpecificListItem = new Specificity(); - - for (const containerElement of childElements) { - 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; - - return specificity; - }; - - //https://www.w3.org/TR/selectors-3/#specificity - const calculateScore = (node: nodes.Node): Specificity => { - 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(); - const childElements = element.getChildren(); - - if (this.isPseudoElementIdentifier(text)) { - if (text.match(/^::slotted/i) && childElements.length > 0) { - // The specificity of ::slotted() is that of a pseudo-element, plus the specificity of its argument. - // ::slotted() does not allow a selector list as its argument, but this isn't the right place to give feedback on validity. - // Reporting the most specific child will be correct for correct CSS and will be forgiving in case of mistakes. - specificity.tag++; - - let mostSpecificListItem = calculateMostSpecificListItem(childElements); - - specificity.id += mostSpecificListItem.id; - specificity.attr += mostSpecificListItem.attr; - specificity.tag += mostSpecificListItem.tag; - continue elementLoop; - } - - 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) && childElements.length > 0) { - let mostSpecificListItem = calculateMostSpecificListItem(childElements); - - specificity.id += mostSpecificListItem.id; - specificity.attr += mostSpecificListItem.attr; - specificity.tag += mostSpecificListItem.tag; - continue elementLoop; - } - - if (text.match(/^:(?:host|host-context)/i) && childElements.length > 0) { - // The specificity of :host() is that of a pseudo-class, plus the specificity of its argument. - // The specificity of :host-context() is that of a pseudo-class, plus the specificity of its argument. - specificity.attr++; - - let mostSpecificListItem = calculateMostSpecificListItem(childElements); - - specificity.id += mostSpecificListItem.id; - specificity.attr += mostSpecificListItem.attr; - specificity.tag += mostSpecificListItem.tag; - continue elementLoop; - } - - if (text.match(/^:(?:nth-child|nth-last-child)/i) && childElements.length > 0) { - /* The specificity of the :nth-child(An+B [of S]?) pseudo-class is the specificity of a single pseudo-class plus, if S is specified, the specificity of the most specific complex selector in S */ - // https://www.w3.org/TR/selectors-4/#the-nth-child-pseudo - specificity.attr++; - - // 23 = Binary Expression. - if (childElements.length === 3 && childElements[1].type === 23) { - let mostSpecificListItem = calculateMostSpecificListItem(childElements[2].getChildren()); - - specificity.id += mostSpecificListItem.id; - specificity.attr += mostSpecificListItem.attr; - specificity.tag += mostSpecificListItem.tag; - - continue elementLoop; - } - - // Edge case: 'n' without integer prefix A, with B integer non-existent, is not regarded as a binary expression token. - const parser = new Parser(); - const pseudoSelectorText = childElements[1].getText(); - parser.scanner.setSource(pseudoSelectorText); - const firstToken = parser.scanner.scan(); - const secondToken = parser.scanner.scan(); - - if (firstToken.text === 'n' || firstToken.text === '-n' && secondToken.text === 'of') { - const complexSelectorListNodes: nodes.Node[] = []; - const complexSelectorText = pseudoSelectorText.slice(secondToken.offset + 2); - const complexSelectorArray = complexSelectorText.split(','); - - for (const selector of complexSelectorArray) { - const node = parser.internalParse(selector, parser._parseSelector); - if (node) { - complexSelectorListNodes.push(node); - } - } - - let mostSpecificListItem = calculateMostSpecificListItem(complexSelectorListNodes); - - specificity.id += mostSpecificListItem.id; - specificity.attr += mostSpecificListItem.attr; - specificity.tag += mostSpecificListItem.tag; - continue elementLoop; - } - - 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})`; - } - + constructor(private cssDataManager: CSSDataManager) {} + + public selectorToMarkedString(node: nodes.Selector, flagOpts?: { isMedia: boolean; text: string }): MarkedString[] { + const root = selectorToElement(node); + if (root) { + const markedStrings = new MarkedStringPrinter('"').print(root, flagOpts); + markedStrings.push(this.selectorToSpecificityMarkedString(node)); + return markedStrings; + } else { + return []; + } + } + + public simpleSelectorToMarkedString(node: nodes.SimpleSelector): MarkedString[] { + const element = toElement(node); + const markedStrings = new MarkedStringPrinter('"').print(element); + markedStrings.push(this.selectorToSpecificityMarkedString(node)); + return markedStrings; + } + + private isPseudoElementIdentifier(text: string): boolean { + const match = text.match(/^::?([\w-]+)/); + + if (!match) { + return false; + } + + return !!this.cssDataManager.getPseudoElement('::' + match[1]); + } + + private selectorToSpecificityMarkedString(node: nodes.Node): MarkedString { + const calculateMostSpecificListItem = (childElements: Array): Specificity => { + const specificity = new Specificity(); + + let mostSpecificListItem = new Specificity(); + + for (const containerElement of childElements) { + 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; + + return specificity; + }; + + //https://www.w3.org/TR/selectors-3/#specificity + const calculateScore = (node: nodes.Node): Specificity => { + 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(); + const childElements = element.getChildren(); + + if (this.isPseudoElementIdentifier(text)) { + if (text.match(/^::slotted/i) && childElements.length > 0) { + // The specificity of ::slotted() is that of a pseudo-element, plus the specificity of its argument. + // ::slotted() does not allow a selector list as its argument, but this isn't the right place to give feedback on validity. + // Reporting the most specific child will be correct for correct CSS and will be forgiving in case of mistakes. + specificity.tag++; + + let mostSpecificListItem = calculateMostSpecificListItem(childElements); + + specificity.id += mostSpecificListItem.id; + specificity.attr += mostSpecificListItem.attr; + specificity.tag += mostSpecificListItem.tag; + continue elementLoop; + } + + 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) && childElements.length > 0) { + let mostSpecificListItem = calculateMostSpecificListItem(childElements); + + specificity.id += mostSpecificListItem.id; + specificity.attr += mostSpecificListItem.attr; + specificity.tag += mostSpecificListItem.tag; + continue elementLoop; + } + + if (text.match(/^:(?:host|host-context)/i) && childElements.length > 0) { + // The specificity of :host() is that of a pseudo-class, plus the specificity of its argument. + // The specificity of :host-context() is that of a pseudo-class, plus the specificity of its argument. + specificity.attr++; + + let mostSpecificListItem = calculateMostSpecificListItem(childElements); + + specificity.id += mostSpecificListItem.id; + specificity.attr += mostSpecificListItem.attr; + specificity.tag += mostSpecificListItem.tag; + continue elementLoop; + } + + if (text.match(/^:(?:nth-child|nth-last-child)/i) && childElements.length > 0) { + /* The specificity of the :nth-child(An+B [of S]?) pseudo-class is the specificity of a single pseudo-class plus, if S is specified, the specificity of the most specific complex selector in S */ + // https://www.w3.org/TR/selectors-4/#the-nth-child-pseudo + specificity.attr++; + + // 23 = Binary Expression. + if (childElements.length === 3 && childElements[1].type === 23) { + let mostSpecificListItem = calculateMostSpecificListItem(childElements[2].getChildren()); + + specificity.id += mostSpecificListItem.id; + specificity.attr += mostSpecificListItem.attr; + specificity.tag += mostSpecificListItem.tag; + + continue elementLoop; + } + + // Edge case: 'n' without integer prefix A, with B integer non-existent, is not regarded as a binary expression token. + const parser = new Parser(); + const pseudoSelectorText = childElements[1].getText(); + parser.scanner.setSource(pseudoSelectorText); + const firstToken = parser.scanner.scan(); + const secondToken = parser.scanner.scan(); + + if (firstToken.text === 'n' || (firstToken.text === '-n' && secondToken.text === 'of')) { + const complexSelectorListNodes: nodes.Node[] = []; + const complexSelectorText = pseudoSelectorText.slice(secondToken.offset + 2); + const complexSelectorArray = complexSelectorText.split(','); + + for (const selector of complexSelectorArray) { + const node = parser.internalParse(selector, parser._parseSelector); + if (node) { + complexSelectorListNodes.push(node); + } + } + + let mostSpecificListItem = calculateMostSpecificListItem(complexSelectorListNodes); + + specificity.id += mostSpecificListItem.id; + specificity.attr += mostSpecificListItem.attr; + specificity.tag += mostSpecificListItem.tag; + continue elementLoop; + } + + 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 { - - private prev: nodes.Node | null; - private element: Element; - - public constructor(element: Element) { - this.prev = null; - this.element = element; - } - - public processSelector(selector: nodes.Selector): void { - let parentElement: Element | null = 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; - } - } - } + private prev: nodes.Node | null; + private element: Element; + + public constructor(element: Element) { + this.prev = null; + this.element = element; + } + + public processSelector(selector: nodes.Selector): void { + let parentElement: Element | null = 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: nodes.Node): boolean { - switch (node.type) { - case nodes.NodeType.MixinDeclaration: - case nodes.NodeType.Stylesheet: - return true; - } - return false; + switch (node.type) { + case nodes.NodeType.MixinDeclaration: + case nodes.NodeType.Stylesheet: + return true; + } + return false; } export function selectorToElement(node: nodes.Selector): Element | null { - if (node.matches('@at-root')) { - return null; - } - const root: Element = new RootElement(); - const parentRuleSets: nodes.RuleSet[] = []; - 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; + if (node.matches('@at-root')) { + return null; + } + const root: Element = new RootElement(); + const parentRuleSets: nodes.RuleSet[] = []; + 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; } diff --git a/src/test/css/hover.test.ts b/src/test/css/hover.test.ts index 9b3f6e25..d4b08ad4 100644 --- a/src/test/css/hover.test.ts +++ b/src/test/css/hover.test.ts @@ -10,98 +10,103 @@ import { Hover, TextDocument, getCSSLanguageService, getLESSLanguageService, get import { HoverSettings } from '../../cssLanguageTypes'; function assertHover(value: string, expected: Hover, languageId = 'css', hoverSettings?: HoverSettings): void { - let offset = value.indexOf('|'); - value = value.substr(0, offset) + value.substr(offset + 1); - const ls = languageId === 'css' ? getCSSLanguageService() : languageId === 'less' ? getLESSLanguageService() : getSCSSLanguageService(); + let offset = value.indexOf('|'); + value = value.substr(0, offset) + value.substr(offset + 1); + const ls = languageId === 'css' ? getCSSLanguageService() : languageId === 'less' ? getLESSLanguageService() : getSCSSLanguageService(); - const document = TextDocument.create(`test://foo/bar.${languageId}`, languageId, 1, value); - const hoverResult = ls.doHover(document, document.positionAt(offset), ls.parseStylesheet(document), hoverSettings); - assert(hoverResult); + const document = TextDocument.create(`test://foo/bar.${languageId}`, languageId, 1, value); + const hoverResult = ls.doHover(document, document.positionAt(offset), ls.parseStylesheet(document), hoverSettings); + assert(hoverResult); - if (hoverResult!.range && expected.range) { - assert.equal(hoverResult!.range, expected.range); - } - assert.deepEqual(hoverResult!.contents, expected.contents); + if (hoverResult!.range && expected.range) { + assert.equal(hoverResult!.range, expected.range); + } + assert.deepEqual(hoverResult!.contents, expected.contents); } suite('CSS Hover', () => { - test('basic', () => { - assertHover('.test { |color: blue; }', { - contents: { - kind: 'markdown', - value: - "Sets the color of an element's text\n\n(Edge 12, Firefox 1, Safari 1, Chrome 1, IE 3, Opera 3)\n\nSyntax: <color>\n\n[MDN Reference](https://developer.mozilla.org/docs/Web/CSS/color)" - } - }); - assertHover('.test { |color: blue; }', { - contents: { - kind: 'markdown', - value: - "[MDN Reference](https://developer.mozilla.org/docs/Web/CSS/color)" - } - }, undefined, { documentation: false }); - assertHover('.test { |color: blue; }', { - contents: { - kind: 'markdown', - value: - "Sets the color of an element's text\n\n(Edge 12, Firefox 1, Safari 1, Chrome 1, IE 3, Opera 3)\n\nSyntax: <color>" - } - }, undefined, { references: false }); + test('basic', () => { + assertHover('.test { |color: blue; }', { + contents: { + kind: 'markdown', + value: + "Sets the color of an element's text\n\n(Edge 12, Firefox 1, Safari 1, Chrome 1, IE 3, Opera 3)\n\nSyntax: <color>\n\n[MDN Reference](https://developer.mozilla.org/docs/Web/CSS/color)", + }, + }); + assertHover( + '.test { |color: blue; }', + { + contents: { + kind: 'markdown', + value: '[MDN Reference](https://developer.mozilla.org/docs/Web/CSS/color)', + }, + }, + undefined, + { documentation: false }, + ); + assertHover( + '.test { |color: blue; }', + { + contents: { + kind: 'markdown', + value: "Sets the color of an element's text\n\n(Edge 12, Firefox 1, Safari 1, Chrome 1, IE 3, Opera 3)\n\nSyntax: <color>", + }, + }, + undefined, + { references: false }, + ); - /** - * Reenable after converting specificity to use MarkupContent - */ + /** + * Reenable after converting specificity to use MarkupContent + */ - // assertHover('.test:h|over { color: blue; }', { - // contents: `Applies while the user designates an element with a pointing device, but does not necessarily activate it. For example, a visual user agent could apply this pseudo-class when the cursor (mouse pointer) hovers over a box generated by the element.` - // }); + // assertHover('.test:h|over { color: blue; }', { + // contents: `Applies while the user designates an element with a pointing device, but does not necessarily activate it. For example, a visual user agent could apply this pseudo-class when the cursor (mouse pointer) hovers over a box generated by the element.` + // }); - // assertHover('.test::a|fter { color: blue; }', { - // contents: `Represents a styleable child pseudo-element immediately after the originating element’s actual content.` - // }); - }); + // assertHover('.test::a|fter { color: blue; }', { + // contents: `Represents a styleable child pseudo-element immediately after the originating element’s actual content.` + // }); + }); - test('specificity', () => { - assertHover('.|foo {}', { - contents: [ - { language: 'html', value: '' }, - '[Selector Specificity](https://developer.mozilla.org/docs/Web/CSS/Specificity): (0, 1, 0)' - ] - }); - }); + test('specificity', () => { + assertHover('.|foo {}', { + contents: [{ language: 'html', value: '' }, '[Selector Specificity](https://developer.mozilla.org/docs/Web/CSS/Specificity): (0, 1, 0)'], + }); + }); }); suite('SCSS Hover', () => { - test('nested', () => { - assertHover( - 'div { d|iv {} }', - { - contents: [ - { language: 'html', value: '
\n …\n
' }, - '[Selector Specificity](https://developer.mozilla.org/docs/Web/CSS/Specificity): (0, 0, 1)' - ] - }, - 'scss' - ); - assertHover( - '.foo{ .bar{ @media only screen{ .|bar{ } } } }', - { - contents: [ - { language: 'html', value: '@media only screen\n … \n …\n \n …\n ' }, - '[Selector Specificity](https://developer.mozilla.org/docs/Web/CSS/Specificity): (0, 1, 0)' - ] - }, - 'scss' - ); - }); + test('nested', () => { + assertHover( + 'div { d|iv {} }', + { + contents: [{ language: 'html', value: '
\n …\n
' }, '[Selector Specificity](https://developer.mozilla.org/docs/Web/CSS/Specificity): (0, 0, 1)'], + }, + 'scss', + ); + assertHover( + '.foo{ .bar{ @media only screen{ .|bar{ } } } }', + { + contents: [ + { + language: 'html', + value: '@media only screen\n … \n …\n \n …\n ', + }, + '[Selector Specificity](https://developer.mozilla.org/docs/Web/CSS/Specificity): (0, 1, 0)', + ], + }, + 'scss', + ); + }); - test('@at-root', () => { - assertHover( - '.test { @|at-root { }', - { - contents: [] - }, - 'scss' - ); - }); + test('@at-root', () => { + assertHover( + '.test { @|at-root { }', + { + contents: [], + }, + 'scss', + ); + }); });