Include pixel equivalents in more places (#775)

* Show pixel equivalents for more `rem`/`em` values

* Add pixel equivalents to media query variant completions
master
Brad Cornes 2023-05-02 10:44:37 +01:00 committed by GitHub
parent 5e9cce64ef
commit 460d921ca3
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 263 additions and 47 deletions

64
package-lock.json generated
View File

@ -6,6 +6,9 @@
"": { "": {
"name": "root", "name": "root",
"dependencies": { "dependencies": {
"@csstools/css-parser-algorithms": "2.1.1",
"@csstools/css-tokenizer": "2.1.1",
"@csstools/media-query-list-parser": "2.0.4",
"@parcel/watcher": "2.0.3", "@parcel/watcher": "2.0.3",
"@tailwindcss/aspect-ratio": "0.4.2", "@tailwindcss/aspect-ratio": "0.4.2",
"@tailwindcss/container-queries": "0.1.0", "@tailwindcss/container-queries": "0.1.0",
@ -46,6 +49,7 @@
"postcss": "8.3.9", "postcss": "8.3.9",
"postcss-load-config": "3.0.1", "postcss-load-config": "3.0.1",
"postcss-selector-parser": "6.0.2", "postcss-selector-parser": "6.0.2",
"postcss-value-parser": "4.2.0",
"prettier": "2.3.0", "prettier": "2.3.0",
"resolve": "1.20.0", "resolve": "1.20.0",
"rimraf": "3.0.2", "rimraf": "3.0.2",
@ -1716,6 +1720,49 @@
"node": ">=0.1.95" "node": ">=0.1.95"
} }
}, },
"node_modules/@csstools/css-parser-algorithms": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/@csstools/css-parser-algorithms/-/css-parser-algorithms-2.1.1.tgz",
"integrity": "sha512-viRnRh02AgO4mwIQb2xQNJju0i+Fh9roNgmbR5xEuG7J3TGgxjnE95HnBLgsFJOJOksvcfxOUCgODcft6Y07cA==",
"engines": {
"node": "^14 || ^16 || >=18"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/csstools"
},
"peerDependencies": {
"@csstools/css-tokenizer": "^2.1.1"
}
},
"node_modules/@csstools/css-tokenizer": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/@csstools/css-tokenizer/-/css-tokenizer-2.1.1.tgz",
"integrity": "sha512-GbrTj2Z8MCTUv+52GE0RbFGM527xuXZ0Xa5g0Z+YN573uveS4G0qi6WNOMyz3yrFM/jaILTTwJ0+umx81EzqfA==",
"engines": {
"node": "^14 || ^16 || >=18"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/csstools"
}
},
"node_modules/@csstools/media-query-list-parser": {
"version": "2.0.4",
"resolved": "https://registry.npmjs.org/@csstools/media-query-list-parser/-/media-query-list-parser-2.0.4.tgz",
"integrity": "sha512-GyYot6jHgcSDZZ+tLSnrzkR7aJhF2ZW6d+CXH66mjy5WpAQhZD4HDke2OQ36SivGRWlZJpAz7TzbW6OKlEpxAA==",
"engines": {
"node": "^14 || ^16 || >=18"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/csstools"
},
"peerDependencies": {
"@csstools/css-parser-algorithms": "^2.1.1",
"@csstools/css-tokenizer": "^2.1.1"
}
},
"node_modules/@evocateur/libnpmaccess": { "node_modules/@evocateur/libnpmaccess": {
"version": "3.1.2", "version": "3.1.2",
"resolved": "https://registry.npmjs.org/@evocateur/libnpmaccess/-/libnpmaccess-3.1.2.tgz", "resolved": "https://registry.npmjs.org/@evocateur/libnpmaccess/-/libnpmaccess-3.1.2.tgz",
@ -22936,6 +22983,23 @@
"minimist": "^1.2.0" "minimist": "^1.2.0"
} }
}, },
"@csstools/css-parser-algorithms": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/@csstools/css-parser-algorithms/-/css-parser-algorithms-2.1.1.tgz",
"integrity": "sha512-viRnRh02AgO4mwIQb2xQNJju0i+Fh9roNgmbR5xEuG7J3TGgxjnE95HnBLgsFJOJOksvcfxOUCgODcft6Y07cA==",
"requires": {}
},
"@csstools/css-tokenizer": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/@csstools/css-tokenizer/-/css-tokenizer-2.1.1.tgz",
"integrity": "sha512-GbrTj2Z8MCTUv+52GE0RbFGM527xuXZ0Xa5g0Z+YN573uveS4G0qi6WNOMyz3yrFM/jaILTTwJ0+umx81EzqfA=="
},
"@csstools/media-query-list-parser": {
"version": "2.0.4",
"resolved": "https://registry.npmjs.org/@csstools/media-query-list-parser/-/media-query-list-parser-2.0.4.tgz",
"integrity": "sha512-GyYot6jHgcSDZZ+tLSnrzkR7aJhF2ZW6d+CXH66mjy5WpAQhZD4HDke2OQ36SivGRWlZJpAz7TzbW6OKlEpxAA==",
"requires": {}
},
"@evocateur/libnpmaccess": { "@evocateur/libnpmaccess": {
"version": "3.1.2", "version": "3.1.2",
"resolved": "https://registry.npmjs.org/@evocateur/libnpmaccess/-/libnpmaccess-3.1.2.tgz", "resolved": "https://registry.npmjs.org/@evocateur/libnpmaccess/-/libnpmaccess-3.1.2.tgz",

View File

@ -14,6 +14,9 @@
"prepublishOnly": "npm run build" "prepublishOnly": "npm run build"
}, },
"dependencies": { "dependencies": {
"@csstools/media-query-list-parser": "2.0.4",
"@csstools/css-parser-algorithms": "2.1.1",
"@csstools/css-tokenizer": "2.1.1",
"@types/culori": "^2.0.0", "@types/culori": "^2.0.0",
"@types/moo": "0.5.3", "@types/moo": "0.5.3",
"@types/semver": "7.3.10", "@types/semver": "7.3.10",
@ -28,6 +31,7 @@
"moo": "0.5.1", "moo": "0.5.1",
"postcss": "8.3.9", "postcss": "8.3.9",
"postcss-selector-parser": "6.0.2", "postcss-selector-parser": "6.0.2",
"postcss-value-parser": "4.2.0",
"semver": "7.3.7", "semver": "7.3.7",
"sift-string": "0.0.2", "sift-string": "0.0.2",
"stringify-object": "3.3.0", "stringify-object": "3.3.0",

View File

@ -28,11 +28,14 @@ import { ensureArray } from './util/array'
import { getClassAttributeLexer, getComputedClassAttributeLexer } from './util/lexers' import { getClassAttributeLexer, getComputedClassAttributeLexer } from './util/lexers'
import { validateApply } from './util/validateApply' import { validateApply } from './util/validateApply'
import { flagEnabled } from './util/flagEnabled' import { flagEnabled } from './util/flagEnabled'
import { remToPx } from './util/remToPx'
import * as jit from './util/jit' import * as jit from './util/jit'
import { getVariantsFromClassName } from './util/getVariantsFromClassName' import { getVariantsFromClassName } from './util/getVariantsFromClassName'
import * as culori from 'culori' import * as culori from 'culori'
import Regex from 'becke-ch--regex--s0-0-v1--base--pl--lib' import Regex from 'becke-ch--regex--s0-0-v1--base--pl--lib'
import {
addPixelEquivalentsToMediaQuery,
addPixelEquivalentsToValue,
} from './util/pixelEquivalents'
let isUtil = (className) => let isUtil = (className) =>
Array.isArray(className.__info) Array.isArray(className.__info)
@ -43,6 +46,7 @@ export function completionsFromClassList(
state: State, state: State,
classList: string, classList: string,
classListRange: Range, classListRange: Range,
rootFontSize: number,
filter?: (item: CompletionItem) => boolean, filter?: (item: CompletionItem) => boolean,
context?: CompletionContext context?: CompletionContext
): CompletionList { ): CompletionList {
@ -190,7 +194,10 @@ export function completionsFromClassList(
items.push( items.push(
variantItem({ variantItem({
label: `${variant.name}${sep}`, label: `${variant.name}${sep}`,
detail: variant.selectors().join(', '), detail: variant
.selectors()
.map((selector) => addPixelEquivalentsToMediaQuery(selector, rootFontSize))
.join(', '),
textEditText: resultingVariants[resultingVariants.length - 1] + sep, textEditText: resultingVariants[resultingVariants.length - 1] + sep,
additionalTextEdits: additionalTextEdits:
shouldSortVariants && resultingVariants.length > 1 shouldSortVariants && resultingVariants.length > 1
@ -430,10 +437,9 @@ async function provideClassAttributeCompletions(
end: position, end: position,
}) })
let matches = matchClassAttributes( let settings = (await state.editor.getConfiguration(document.uri)).tailwindCSS
str,
(await state.editor.getConfiguration(document.uri)).tailwindCSS.classAttributes let matches = matchClassAttributes(str, settings.classAttributes)
)
if (matches.length === 0) { if (matches.length === 0) {
return null return null
@ -470,6 +476,7 @@ async function provideClassAttributeCompletions(
}, },
end: position, end: position,
}, },
settings.rootFontSize,
undefined, undefined,
context context
) )
@ -544,6 +551,7 @@ async function provideCustomClassNameCompletions(
}, },
end: position, end: position,
}, },
settings.tailwindCSS.rootFontSize,
undefined, undefined,
context context
) )
@ -555,12 +563,13 @@ async function provideCustomClassNameCompletions(
return null return null
} }
function provideAtApplyCompletions( async function provideAtApplyCompletions(
state: State, state: State,
document: TextDocument, document: TextDocument,
position: Position, position: Position,
context?: CompletionContext context?: CompletionContext
): CompletionList { ): Promise<CompletionList> {
let settings = (await state.editor.getConfiguration(document.uri)).tailwindCSS
let str = document.getText({ let str = document.getText({
start: { line: Math.max(position.line - 30, 0), character: 0 }, start: { line: Math.max(position.line - 30, 0), character: 0 },
end: position, end: position,
@ -584,6 +593,7 @@ function provideAtApplyCompletions(
}, },
end: position, end: position,
}, },
settings.rootFontSize,
(item) => { (item) => {
if (item.kind === 9) { if (item.kind === 9) {
return ( return (
@ -1318,13 +1328,18 @@ async function provideEmmetCompletions(
const parts = emmetItems.items[0].label.split('.') const parts = emmetItems.items[0].label.split('.')
if (parts.length < 2) return null if (parts.length < 2) return null
return completionsFromClassList(state, parts[parts.length - 1], { return completionsFromClassList(
state,
parts[parts.length - 1],
{
start: { start: {
line: position.line, line: position.line,
character: position.character - parts[parts.length - 1].length, character: position.character - parts[parts.length - 1].length,
}, },
end: position, end: position,
}) },
settings.tailwindCSS.rootFontSize
)
} }
export async function doComplete( export async function doComplete(
@ -1444,10 +1459,10 @@ function stringifyDecls(obj: any, settings: Settings): string {
.map((prop) => .map((prop) =>
ensureArray(obj[prop]) ensureArray(obj[prop])
.map((value) => { .map((value) => {
const px = settings.tailwindCSS.showPixelEquivalents if (settings.tailwindCSS.showPixelEquivalents) {
? remToPx(value, settings.tailwindCSS.rootFontSize) value = addPixelEquivalentsToValue(value, settings.tailwindCSS.rootFontSize)
: undefined }
return `${prop}: ${value}${px ? `/* ${px} */` : ''};` return `${prop}: ${value};`
}) })
.join(' ') .join(' ')
) )

View File

@ -1,6 +1,6 @@
import { State } from './state' import { State } from './state'
import type { Container, Document, Root, Rule, Node, AtRule } from 'postcss' import type { Container, Document, Root, Rule, Node, AtRule } from 'postcss'
import { remToPx } from './remToPx' import { addPixelEquivalentsToCss, addPixelEquivalentsToValue } from './pixelEquivalents'
export function bigSign(bigIntValue) { export function bigSign(bigIntValue) {
// @ts-ignore // @ts-ignore
@ -41,17 +41,13 @@ export async function stringifyRoot(state: State, root: Root, uri?: string): Pro
node.remove() node.remove()
}) })
let css = clone.toString()
if (settings.tailwindCSS.showPixelEquivalents) { if (settings.tailwindCSS.showPixelEquivalents) {
clone.walkDecls((decl) => { css = addPixelEquivalentsToCss(css, settings.tailwindCSS.rootFontSize)
let px = remToPx(decl.value, settings.tailwindCSS.rootFontSize)
if (px) {
decl.value = `${decl.value}/* ${px} */`
}
})
} }
return clone return css
.toString()
.replace(/([^;{}\s])(\n\s*})/g, (_match, before, after) => `${before};${after}`) .replace(/([^;{}\s])(\n\s*})/g, (_match, before, after) => `${before};${after}`)
.replace(/^(?: )+/gm, (indent: string) => .replace(/^(?: )+/gm, (indent: string) =>
' '.repeat((indent.length / 4) * settings.editor.tabSize) ' '.repeat((indent.length / 4) * settings.editor.tabSize)
@ -70,10 +66,10 @@ export async function stringifyDecls(state: State, rule: Rule, uri?: string): Pr
let result = [] let result = []
rule.walkDecls(({ prop, value }) => { rule.walkDecls(({ prop, value }) => {
let px = settings.tailwindCSS.showPixelEquivalents if (settings.tailwindCSS.showPixelEquivalents) {
? remToPx(value, settings.tailwindCSS.rootFontSize) value = addPixelEquivalentsToValue(value, settings.tailwindCSS.rootFontSize)
: undefined }
result.push(`${prop}: ${value}${px ? `/* ${px} */` : ''};`) result.push(`${prop}: ${value};`)
}) })
return result.join(' ') return result.join(' ')
} }

View File

@ -0,0 +1,147 @@
import type { Plugin } from 'postcss'
import parseValue from 'postcss-value-parser'
import { parse as parseMediaQueryList } from '@csstools/media-query-list-parser'
import postcss from 'postcss'
import { isTokenNode } from '@csstools/css-parser-algorithms'
type Comment = { index: number; value: string }
export function addPixelEquivalentsToValue(value: string, rootFontSize: number): string {
if (!value.includes('rem')) {
return value
}
parseValue(value).walk((node) => {
if (node.type !== 'word') {
return true
}
let unit = parseValue.unit(node.value)
if (!unit || unit.unit !== 'rem') {
return false
}
let commentStr = `/* ${parseFloat(unit.number) * rootFontSize}px */`
value = value.slice(0, node.sourceEndIndex) + commentStr + value.slice(node.sourceEndIndex)
return false
})
return value
}
export function addPixelEquivalentsToCss(css: string, rootFontSize: number): string {
if (!css.includes('em')) {
return css
}
let comments: Comment[] = []
try {
postcss([postcssPlugin({ comments, rootFontSize })]).process(css, { from: undefined }).css
} catch {
return css
}
return applyComments(css, comments)
}
function applyComments(str: string, comments: Comment[]): string {
let offset = 0
for (let comment of comments) {
let index = comment.index + offset
let commentStr = `/* ${comment.value} */`
str = str.slice(0, index) + commentStr + str.slice(index)
offset += commentStr.length
}
return str
}
function getPixelEquivalentsForMediaQuery(params: string, rootFontSize: number): Comment[] {
let comments: Comment[] = []
try {
parseMediaQueryList(params).forEach((mediaQuery) => {
mediaQuery.walk(({ node }) => {
if (
isTokenNode(node) &&
node.type === 'token' &&
node.value[0] === 'dimension-token' &&
(node.value[4].type === 'integer' || node.value[4].type === 'number') &&
(node.value[4].unit === 'rem' || node.value[4].unit === 'em')
) {
comments.push({
index: params.length - (params.length - node.value[3] - 1),
value: `${node.value[4].value * rootFontSize}px`,
})
}
})
})
} catch {}
return comments
}
export function addPixelEquivalentsToMediaQuery(query: string, rootFontSize: number): string {
return query.replace(/(?<=^\s*@media\s*).*?$/, (params) => {
let comments = getPixelEquivalentsForMediaQuery(params, rootFontSize)
return applyComments(params, comments)
})
}
function postcssPlugin({
comments,
rootFontSize,
}: {
comments: Comment[]
rootFontSize: number
}): Plugin {
return {
postcssPlugin: 'plugin',
AtRule: {
media(atRule) {
if (!atRule.params.includes('em')) {
return
}
comments.push(
...getPixelEquivalentsForMediaQuery(atRule.params, rootFontSize).map(
({ index, value }) => ({
index: index + atRule.source.start.offset + `@media${atRule.raws.afterName}`.length,
value,
})
)
)
},
},
Declaration(decl) {
if (!decl.value.includes('rem')) {
return
}
parseValue(decl.value).walk((node) => {
if (node.type !== 'word') {
return true
}
let unit = parseValue.unit(node.value)
if (!unit || unit.unit !== 'rem') {
return false
}
comments.push({
index:
decl.source.start.offset +
`${decl.prop}${decl.raws.between}`.length +
node.sourceEndIndex,
value: `${parseFloat(unit.number) * rootFontSize}px`,
})
return false
})
},
}
}
postcssPlugin.postcss = true

View File

@ -1,9 +0,0 @@
export function remToPx(value: string, rootSize: number = 16): string | undefined {
if (/^-?[0-9.]+rem$/.test(value)) {
let number = parseFloat(value.substr(0, value.length - 3))
if (!isNaN(number)) {
return `${number * rootSize}px`
}
}
return undefined
}

View File

@ -2,10 +2,10 @@ import removeMeta from './removeMeta'
import dlv from 'dlv' import dlv from 'dlv'
import escapeClassName from 'css.escape' import escapeClassName from 'css.escape'
import { ensureArray } from './array' import { ensureArray } from './array'
import { remToPx } from './remToPx'
import stringifyObject from 'stringify-object' import stringifyObject from 'stringify-object'
import isObject from './isObject' import isObject from './isObject'
import { Settings } from './state' import { Settings } from './state'
import { addPixelEquivalentsToCss } from './pixelEquivalents'
export function stringifyConfigValue(x: any): string { export function stringifyConfigValue(x: any): string {
if (isObject(x)) return `${Object.keys(x).length} values` if (isObject(x)) return `${Object.keys(x).length} values`
@ -45,12 +45,7 @@ export function stringifyCss(className: string, obj: any, settings: Settings): s
const indentStr = indent.repeat(context.length) const indentStr = indent.repeat(context.length)
const decls = props.reduce((acc, curr, i) => { const decls = props.reduce((acc, curr, i) => {
const propStr = ensureArray(obj[curr]) const propStr = ensureArray(obj[curr])
.map((val) => { .map((val) => `${indentStr + indent}${curr}: ${val};`)
const px = settings.tailwindCSS.showPixelEquivalents
? remToPx(val, settings.tailwindCSS.rootFontSize)
: undefined
return `${indentStr + indent}${curr}: ${val}${px ? `/* ${px} */` : ''};`
})
.join('\n') .join('\n')
return `${acc}${i === 0 ? '' : '\n'}${propStr}` return `${acc}${i === 0 ? '' : '\n'}${propStr}`
}, '') }, '')
@ -60,6 +55,10 @@ export function stringifyCss(className: string, obj: any, settings: Settings): s
css += `${indent.repeat(i)}\n}` css += `${indent.repeat(i)}\n}`
} }
if (settings.tailwindCSS.showPixelEquivalents) {
return addPixelEquivalentsToCss(css, settings.tailwindCSS.rootFontSize)
}
return css return css
} }