diff --git a/packages/tailwindcss-class-names/src/extractClassNames.mjs b/packages/tailwindcss-class-names/src/extractClassNames.mjs index 4ccb6622101ef1538bfffa21842e3265d0706a23..c802e4e6bee916c81bf4ac80acf0f3439471a671 100644 --- a/packages/tailwindcss-class-names/src/extractClassNames.mjs +++ b/packages/tailwindcss-class-names/src/extractClassNames.mjs @@ -18,6 +18,20 @@ const classNames = [] const { nodes: subSelectors } = selectorParser().astSync(selector) for (let i = 0; i < subSelectors.length; i++) { + // const final = subSelectors[i].nodes[subSelectors[i].nodes.length - 1] + + // if (final.type === 'class') { + // const scope = subSelectors[i].nodes.slice( + // 0, + // subSelectors[i].nodes.length - 1 + // ) + + // classNames.push({ + // className: String(final).trim(), + // scope: createSelectorFromNodes(scope) + // }) + // } + let scope = [] for (let j = 0; j < subSelectors[i].nodes.length; j++) { let node = subSelectors[i].nodes[j] @@ -33,28 +47,39 @@ next = subSelectors[i].nodes[j + 1] } classNames.push({ - className: node.value.trim(), + className: String(node) + .trim() + .substr(1), scope: createSelectorFromNodes(scope), __rule: j === subSelectors[i].nodes.length - 1, - __pseudo: pseudo.length === 0 ? null : pseudo.map(String), + // __pseudo: createSelectorFromNodes(pseudo) + __pseudo: pseudo.length === 0 ? null : pseudo.map(String) }) } scope.push(node, ...pseudo) } } + // console.log(classNames) + return classNames } +// console.log(getClassNamesFromSelector('h1, h2, h3, .foo .bar, .baz')) + +// const css = fs.readFileSync(path.resolve(__dirname, 'tailwind.css'), 'utf8') + async function process(ast) { + const start = new Date() + const tree = {} const commonContext = {} - ast.root.walkRules((rule) => { + ast.root.walkRules(rule => { const classNames = getClassNamesFromSelector(rule.selector) - const decls = {} - rule.walkDecls((decl) => { + const decls = { __decls: true } + rule.walkDecls(decl => { decls[decl.prop] = decl.value }) @@ -71,48 +96,49 @@ for (let i = 0; i < classNames.length; i++) { const context = keys.concat([]) const baseKeys = classNames[i].className.split('__TAILWIND_SEPARATOR__') const contextKeys = baseKeys.slice(0, baseKeys.length - 1) - const index = [] - const existing = dlv(tree, baseKeys) - if (typeof existing !== 'undefined') { - if (Array.isArray(existing)) { - const scopeIndex = existing.findIndex( - (x) => - x.__scope === classNames[i].scope && - arraysEqual(existing.__context, context) - ) - if (scopeIndex > -1) { - keys.unshift(scopeIndex) - index.push(scopeIndex) + if (classNames[i].scope) { + let index = [] + const existing = dlv(tree, baseKeys) + if (typeof existing !== 'undefined') { + if (Array.isArray(existing)) { + const scopeIndex = existing.findIndex( + x => x.__scope === classNames[i].scope + ) + if (scopeIndex > -1) { + keys.unshift(scopeIndex) + index.push(scopeIndex) + } else { + keys.unshift(existing.length) + index.push(existing.length) + } } else { - keys.unshift(existing.length) - index.push(existing.length) + if (existing.__scope !== classNames[i].scope) { + dset(tree, baseKeys, [existing]) + keys.unshift(1) + index.push(1) + } } + } + if (classNames[i].__rule) { + dset(tree, [...baseKeys, ...index, '__rule'], true) + dsetEach(tree, [...baseKeys, ...keys], decls) + } + if (classNames[i].__pseudo) { + dset(tree, [...baseKeys, ...keys, '__pseudo'], classNames[i].__pseudo) + } + dset(tree, [...baseKeys, ...index, '__scope'], classNames[i].scope) + } else { + if (classNames[i].__rule) { + dset(tree, [...baseKeys, '__rule'], true) + dsetEach(tree, [...baseKeys, ...keys], decls) } else { - if ( - existing.__scope !== classNames[i].scope || - !arraysEqual(existing.__context, context) - ) { - dset(tree, baseKeys, [existing]) - keys.unshift(1) - index.push(1) - } + dset(tree, [...baseKeys, ...keys], {}) + } + if (classNames[i].__pseudo) { + dset(tree, [...baseKeys, ...keys, '__pseudo'], classNames[i].__pseudo) } } - if (classNames[i].__rule) { - dset(tree, [...baseKeys, ...index, '__rule'], true) - - dsetEach(tree, [...baseKeys, ...index], decls) - } - if (classNames[i].__pseudo) { - dset(tree, [...baseKeys, '__pseudo'], classNames[i].__pseudo) - } - dset(tree, [...baseKeys, ...index, '__scope'], classNames[i].scope) - dset( - tree, - [...baseKeys, ...index, '__context'], - context.concat([]).reverse() - ) // common context if (classNames[i].__pseudo) { @@ -131,12 +157,15 @@ } } } }) + // console.log(`${new Date() - start}ms`) + // console.log(tree) + // console.log(commonContext) return { classNames: tree, context: commonContext } } function intersection(arr1, arr2) { - return arr1.filter((value) => arr2.indexOf(value) !== -1) + return arr1.filter(value => arr2.indexOf(value) !== -1) } function dsetEach(obj, keys, values) { @@ -146,15 +175,14 @@ dset(obj, [...keys, k[i]], values[k[i]]) } } -function arraysEqual(a, b) { - if (a === b) return true - if (a == null || b == null) return false - if (a.length !== b.length) return false +export default process - for (let i = 0; i < a.length; ++i) { - if (a[i] !== b[i]) return false - } - return true -} - -export default process +// process(` +// .bg-red { +// background-color: red; +// } +// .bg-red { +// color: white; +// }`).then(x => { +// console.log(x) +// }) diff --git a/packages/tailwindcss-class-names/tests/extractClassNames.test.js b/packages/tailwindcss-class-names/tests/extractClassNames.test.js index 19290e671a71bea06ee61060cba5c41002db3ce1..b276787c05b815552a7f1ca835d2efa5ac5fe396 100644 --- a/packages/tailwindcss-class-names/tests/extractClassNames.test.js +++ b/packages/tailwindcss-class-names/tests/extractClassNames.test.js @@ -3,73 +3,9 @@ const esmImport = require('esm')(module) const process = esmImport('../src/extractClassNames.mjs').default postcss = postcss([postcss.plugin('no-op', () => () => {})]) -const processCss = async (css) => +const processCss = async css => process(await postcss.process(css, { from: undefined })) -test('processes default container plugin', async () => { - const result = await processCss(` - .container { - width: 100% - } - - @media (min-width: 640px) { - .container { - max-width: 640px - } - } - - @media (min-width: 768px) { - .container { - max-width: 768px - } - } - - @media (min-width: 1024px) { - .container { - max-width: 1024px - } - } - - @media (min-width: 1280px) { - .container { - max-width: 1280px - } - } - `) - expect(result).toEqual({ - context: {}, - classNames: { - container: [ - { __context: [], __rule: true, __scope: null, width: '100%' }, - { - __rule: true, - __scope: null, - __context: ['@media (min-width: 640px)'], - 'max-width': '640px', - }, - { - __rule: true, - __scope: null, - __context: ['@media (min-width: 768px)'], - 'max-width': '768px', - }, - { - __rule: true, - __scope: null, - __context: ['@media (min-width: 1024px)'], - 'max-width': '1024px', - }, - { - __rule: true, - __scope: null, - __context: ['@media (min-width: 1280px)'], - 'max-width': '1280px', - }, - ], - }, - }) -}) - test('foo', async () => { const result = await processCss(` @media (min-width: 640px) { @@ -88,42 +24,43 @@ expect(result).toEqual({ context: { sm: ['@media (min-width: 640px)'], - hover: [':hover'], + hover: [':hover'] }, classNames: { sm: { 'bg-red': { __rule: true, - __scope: null, - __context: ['@media (min-width: 640px)'], - 'background-color': 'red', + '@media (min-width: 640px)': { + __decls: true, + 'background-color': 'red' + } }, hover: { 'bg-red': { __rule: true, - __scope: null, - __context: ['@media (min-width: 640px)'], - __pseudo: [':hover'], - 'background-color': 'red', - }, - }, + '@media (min-width: 640px)': { + __decls: true, + __pseudo: [':hover'], + 'background-color': 'red' + } + } + } }, hover: { 'bg-red': { __rule: true, - __scope: null, + __decls: true, __pseudo: [':hover'], - __context: [], - 'background-color': 'red', - }, - }, - }, + 'background-color': 'red' + } + } + } }) }) -test.only('processes basic css', async () => { +test('processes basic css', async () => { const result = await processCss(` - .bg-red\\:foo { + .bg-red { background-color: red; } `) @@ -133,11 +70,10 @@ context: {}, classNames: { 'bg-red': { __rule: true, - __scope: null, - __context: [], - 'background-color': 'red', - }, - }, + __decls: true, + 'background-color': 'red' + } + } }) }) @@ -153,12 +89,11 @@ context: {}, classNames: { 'bg-red': { __rule: true, - __scope: null, - __context: [], + __decls: true, __pseudo: [':first-child', '::after'], - 'background-color': 'red', - }, - }, + 'background-color': 'red' + } + } }) }) @@ -173,17 +108,15 @@ expect(result).toEqual({ context: {}, classNames: { scope: { - __context: [], - __pseudo: [':hover'], - __scope: null, + __pseudo: [':hover'] }, 'bg-red': { - __context: [], __rule: true, + __decls: true, __scope: '.scope:hover', - 'background-color': 'red', - }, - }, + 'background-color': 'red' + } + } }) }) @@ -200,17 +133,15 @@ context: {}, classNames: { 'bg-red': { __rule: true, - __scope: null, - __context: [], - 'background-color': 'red', + __decls: true, + 'background-color': 'red' }, 'bg-red-again': { __rule: true, - __scope: null, - __context: [], - 'background-color': 'red', - }, - }, + __decls: true, + 'background-color': 'red' + } + } }) }) @@ -228,35 +159,12 @@ context: {}, classNames: { 'bg-red': { __rule: true, - __scope: null, - __context: ['@media (min-width: 768px)'], - 'background-color': 'red', - }, - }, - }) -}) - -test('processes nested at-rules', async () => { - const result = await processCss(` - @supports (display: grid) { - @media (min-width: 768px) { - .bg-red { - background-color: red; + '@media (min-width: 768px)': { + __decls: true, + 'background-color': 'red' } } } - `) - - expect(result).toEqual({ - context: {}, - classNames: { - 'bg-red': { - __rule: true, - __scope: null, - __context: ['@supports (display: grid)', '@media (min-width: 768px)'], - 'background-color': 'red', - }, - }, }) }) @@ -275,12 +183,11 @@ context: {}, classNames: { 'bg-red': { __rule: true, - __scope: null, - __context: [], + __decls: true, 'background-color': 'red', - color: 'white', - }, - }, + color: 'white' + } + } }) }) @@ -294,17 +201,14 @@ expect(result).toEqual({ context: {}, classNames: { - scope: { - __context: [], - __scope: null, - }, + scope: {}, 'bg-red': { __rule: true, - __context: [], + __decls: true, __scope: '.scope', - 'background-color': 'red', - }, - }, + 'background-color': 'red' + } + } }) }) @@ -324,29 +228,29 @@ expect(result).toEqual({ context: {}, classNames: { - scope1: { __context: [], __scope: null }, - scope2: { __context: [], __scope: null }, - scope3: { __context: [], __scope: null }, + scope1: {}, + scope2: {}, + scope3: {}, 'bg-red': [ { __rule: true, - __context: [], + __decls: true, __scope: '.scope1', - 'background-color': 'red', + 'background-color': 'red' }, { __rule: true, - __context: [], + __decls: true, __scope: '.scope2 +', - 'background-color': 'red', + 'background-color': 'red' }, { __rule: true, - __context: [], + __decls: true, __scope: '.scope3 >', - 'background-color': 'red', - }, - ], - }, + 'background-color': 'red' + } + ] + } }) }) diff --git a/packages/tailwindcss-language-server/src/providers/completionProvider.ts b/packages/tailwindcss-language-server/src/providers/completionProvider.ts index 69ba6ec3ed6e77b65602e687cf9b3cdfb12a91c7..84c54c72463959fc711571a6c0c08b6845058f4f 100644 --- a/packages/tailwindcss-language-server/src/providers/completionProvider.ts +++ b/packages/tailwindcss-language-server/src/providers/completionProvider.ts @@ -27,7 +27,6 @@ // TODO let sep = ':' let parts = partialClassName.split(sep) let subset: any - let subsetKey: string[] = [] let isSubset: boolean = false let replacementRange = { @@ -43,7 +42,6 @@ let keys = parts.slice(0, i).filter(Boolean) subset = dlv(state.classNames.classNames, keys) if (typeof subset !== 'undefined' && typeof subset.__rule === 'undefined') { isSubset = true - subsetKey = keys replacementRange = { ...replacementRange, start: { @@ -64,7 +62,7 @@ items: Object.keys(isSubset ? subset : state.classNames.classNames).map( (className) => { let kind: CompletionItemKind = CompletionItemKind.Constant let documentation: string = null - if (isContextItem(state, [...subsetKey, className])) { + if (isContextItem(state, [className])) { kind = CompletionItemKind.Module } else { const color = getColor(state, [className]) @@ -78,7 +76,6 @@ return { label: className, kind, documentation, - data: [...subsetKey, className], textEdit: { newText: className, range: replacementRange, @@ -517,20 +514,20 @@ ) { return item } - const className = dlv(state.classNames.classNames, item.data) - if (isContextItem(state, item.data)) { - item.detail = state.classNames.context[ - item.data[item.data.length - 1] - ].join(', ') + const className = state.classNames.classNames[item.label] + if (isContextItem(state, [item.label])) { + item.detail = state.classNames.context[item.label].join(', ') } else { item.detail = getCssDetail(state, className) if (!item.documentation) { - const css = stringifyCss(item.data.join(':'), className) - if (css) { - item.documentation = { - kind: MarkupKind.Markdown, - value: ['```css', css, '```'].join('\n'), - } + item.documentation = stringifyCss(className) + if (item.detail === item.documentation) { + item.documentation = null + } else { + // item.documentation = { + // kind: MarkupKind.Markdown, + // value: ['```css', item.documentation, '```'].join('\n') + // } } } } @@ -540,8 +537,7 @@ function isContextItem(state: State, keys: string[]): boolean { const item = dlv(state.classNames.classNames, keys) return Boolean( - isObject(item) && - !item.__rule && + !item.__rule && !Array.isArray(item) && state.classNames.context[keys[keys.length - 1]] ) @@ -559,8 +555,13 @@ function getCssDetail(state: State, className: any): string { if (Array.isArray(className)) { return `${className.length} rules` } - if (className.__rule === true) { - return stringifyDecls(removeMeta(className)) + let withoutMeta = removeMeta(className) + if (className.__decls === true) { + return stringifyDecls(withoutMeta) } - return null + let keys = Object.keys(withoutMeta) + if (keys.length === 1) { + return getCssDetail(state, className[keys[0]]) + } + return `${keys.length} rules` } diff --git a/packages/tailwindcss-language-server/src/providers/hoverProvider.ts b/packages/tailwindcss-language-server/src/providers/hoverProvider.ts index 20147baff96b89fdf928f536d26634892e9ca10e..5ce792101bf49e000c10ce9bfb4523f44eda5827 100644 --- a/packages/tailwindcss-language-server/src/providers/hoverProvider.ts +++ b/packages/tailwindcss-language-server/src/providers/hoverProvider.ts @@ -6,6 +6,7 @@ getClassNameParts, } from '../util/getClassNameAtPosition' import { stringifyCss, stringifyConfigValue } from '../util/stringify' const dlv = require('dlv') +import escapeClassName from 'css.escape' import { isHtmlContext } from '../util/html' import { isCssContext } from '../util/css' @@ -89,11 +90,21 @@ return { contents: { language: 'css', - value: stringifyCss( - hovered.className, - dlv(state.classNames.classNames, parts) - ), + value: stringifyCss(dlv(state.classNames.classNames, parts), { + selector: augmentClassName(parts, state), + }), }, range: hovered.range, } } + +// TODO +function augmentClassName(className: string | string[], state: State): string { + const parts = Array.isArray(className) + ? className + : getClassNameParts(state, className) + const obj = dlv(state.classNames.classNames, parts) + const pseudo = obj.__pseudo ? obj.__pseudo.join('') : '' + const scope = obj.__scope ? `${obj.__scope} ` : '' + return `${scope}.${escapeClassName(parts.join(state.separator))}${pseudo}` +} diff --git a/packages/tailwindcss-language-server/src/util/color.ts b/packages/tailwindcss-language-server/src/util/color.ts index bb44bba0b14ca9d8ed3c17e67d8f16186164a834..0ac2f8a32fdda94058a64b233e2b834f5dd9ef4e 100644 --- a/packages/tailwindcss-language-server/src/util/color.ts +++ b/packages/tailwindcss-language-server/src/util/color.ts @@ -16,7 +16,7 @@ 'fill', 'outline-color', 'stop-color', 'stroke', - 'text-decoration-color', + 'text-decoration-color' ] const COLOR_NAMES = { @@ -169,12 +169,12 @@ wheat: '#f5deb3', white: '#fff', whitesmoke: '#f5f5f5', yellow: '#ff0', - yellowgreen: '#9acd32', + yellowgreen: '#9acd32' } export function getColor(state: State, keys: string[]): string { const item = dlv(state.classNames.classNames, keys) - if (!item.__rule) return null + if (!item.__decls) return null const props = Object.keys(removeMeta(item)) if (props.length === 0 || props.length > 1) return null const prop = props[0] diff --git a/packages/tailwindcss-language-server/src/util/getClassNameAtPosition.ts b/packages/tailwindcss-language-server/src/util/getClassNameAtPosition.ts index e7e0cf2cf4255e2e420c816dfc9af8e747ed265c..709ca96fa29845b5d89327b67a8866f1211fb520 100644 --- a/packages/tailwindcss-language-server/src/util/getClassNameAtPosition.ts +++ b/packages/tailwindcss-language-server/src/util/getClassNameAtPosition.ts @@ -49,8 +49,7 @@ className = className.replace(/^\./, '') let parts: string[] = className.split(separator) if (parts.length === 1) { - return dlv(state.classNames.classNames, [className, '__rule']) === true || - Array.isArray(dlv(state.classNames.classNames, [className])) + return dlv(state.classNames.classNames, [className, '__rule']) === true ? [className] : null } @@ -74,10 +73,7 @@ }), ] return possibilities.find((key) => { - if ( - dlv(state.classNames.classNames, [...key, '__rule']) === true || - Array.isArray(dlv(state.classNames.classNames, [...key])) - ) { + if (dlv(state.classNames.classNames, [...key, '__rule']) === true) { return true } return false diff --git a/packages/tailwindcss-language-server/src/util/stringify.ts b/packages/tailwindcss-language-server/src/util/stringify.ts index 5433e90926433db92e6f14389dd4c1d796df38a3..e8b1b7d7bb5ba7eb3c4a25f4de5ed709e463c313 100644 --- a/packages/tailwindcss-language-server/src/util/stringify.ts +++ b/packages/tailwindcss-language-server/src/util/stringify.ts @@ -1,6 +1,4 @@ import removeMeta from './removeMeta' -const dlv = require('dlv') -import escapeClassName from 'css.escape' export function stringifyConfigValue(x: any): string { if (typeof x === 'string') return x @@ -14,45 +12,34 @@ } return null } -export function stringifyCss(className: string, obj: any): string { - if (obj.__rule !== true && !Array.isArray(obj)) return null - - if (Array.isArray(obj)) { - const rules = obj.map((x) => stringifyCss(className, x)).filter(Boolean) - if (rules.length === 0) return null - return rules.join('\n\n') +export function stringifyCss( + obj: any, + { indent = 0, selector }: { indent?: number; selector?: string } = {} +): string { + let indentStr = '\t'.repeat(indent) + if (obj.__decls === true) { + let before = '' + let after = '' + if (selector) { + before = `${indentStr}${selector} {\n` + after = `\n${indentStr}}` + indentStr += '\t' + } + return ( + before + + Object.keys(removeMeta(obj)).reduce((acc, curr, i) => { + return `${acc}${i === 0 ? '' : '\n'}${indentStr}${curr}: ${obj[curr]};` + }, '') + + after + ) } - - let css = `` - - const context = dlv(obj, '__context', []) - const props = Object.keys(removeMeta(obj)) - if (props.length === 0) return null - - for (let i = 0; i < context.length; i++) { - css += `${'\t'.repeat(i)}${context[i]} {\n` - } - - const indentStr = '\t'.repeat(context.length) - const decls = props.reduce((acc, curr, i) => { - return `${acc}${i === 0 ? '' : '\n'}${indentStr + '\t'}${curr}: ${ - obj[curr] - };` + return Object.keys(removeMeta(obj)).reduce((acc, curr, i) => { + return `${acc}${i === 0 ? '' : '\n'}${indentStr}${curr} {\n${stringifyCss( + obj[curr], + { + indent: indent + 1, + selector, + } + )}\n${indentStr}}` }, '') - css += `${indentStr}${augmentClassName( - className, - obj - )} {\n${decls}\n${indentStr}}` - - for (let i = context.length - 1; i >= 0; i--) { - css += `${'\t'.repeat(i)}\n}` - } - - return css -} - -function augmentClassName(className: string, obj: any): string { - const pseudo = obj.__pseudo ? obj.__pseudo.join('') : '' - const scope = obj.__scope ? `${obj.__scope} ` : '' - return `${scope}.${escapeClassName(className)}${pseudo}` }