diff --git a/packages/core/src/extractors/index.ts b/packages/core/src/extractors/index.ts index ecec67de51..bc4541d0b4 100644 --- a/packages/core/src/extractors/index.ts +++ b/packages/core/src/extractors/index.ts @@ -1,2 +1,2 @@ -export { extractorSplit } from './split' +export { extractorSplit, cssPropertyRE } from './split' export { extractorSvelte } from './svelte' diff --git a/packages/core/src/extractors/split.ts b/packages/core/src/extractors/split.ts index 899e83a345..760a51db50 100644 --- a/packages/core/src/extractors/split.ts +++ b/packages/core/src/extractors/split.ts @@ -1,7 +1,21 @@ import type { Extractor } from '../types' import { isValidSelector } from '../utils' -export const splitCode = (code: string) => [...new Set(code.split(/\\?[\s'"`;{}]+/g))].filter(isValidSelector) +const defaultSplitRE = /\\?[\s'"`;{}]+/g +export const cssPropertyRE = /[\s'"`](\[(\\\W|[\w-])+:['"]?\S*?['"]?\])/g + +export const splitCode = (code: string) => { + const result = new Set() + + for (const match of code.matchAll(cssPropertyRE)) + result.add(match[1]) + + code.split(defaultSplitRE).forEach((match) => { + isValidSelector(match) && result.add(match) + }) + + return [...result] +} export const extractorSplit: Extractor = { name: 'split', diff --git a/packages/preset-mini/src/_rules/variables.ts b/packages/preset-mini/src/_rules/variables.ts index f851bc54af..2fa2ec0bbe 100644 --- a/packages/preset-mini/src/_rules/variables.ts +++ b/packages/preset-mini/src/_rules/variables.ts @@ -26,5 +26,5 @@ export const cssVariables: Rule[] = [ ] export const cssProperty: Rule[] = [ - [/^\[([\w_-]+):([^'"]+)\]$/, ([, prop, value]) => ({ [prop]: h.bracket(`[${value}]`) })], + [/^\[(--(\w|\\\W)+|[\w-]+):(.+)\]$/, ([, prop,, value]) => ({ [prop]: h.bracket(`[${value}]`) })], ] diff --git a/packages/shared-common/src/index.ts b/packages/shared-common/src/index.ts index 59005d0077..25d5229b6d 100644 --- a/packages/shared-common/src/index.ts +++ b/packages/shared-common/src/index.ts @@ -1,5 +1,5 @@ import type { UnoGenerator } from '@unocss/core' -import { escapeRegExp, isAttributifySelector, regexClassGroup } from '@unocss/core' +import { cssPropertyRE, escapeRegExp, isAttributifySelector, regexClassGroup } from '@unocss/core' import MagicString from 'magic-string' // https://github.com/dsblv/string-replace-async/blob/main/index.js @@ -53,6 +53,14 @@ export function getMatchedPositions(code: string, matched: string[], hasVariantG start = end }) + // highlight for arbitrary css properties + for (const match of code.matchAll(cssPropertyRE)) { + const start = match.index! + 1 + const end = start + match[1].length + if (plain.has(match[1])) + result.push([start, end, match[1]]) + } + // highlight for variant group if (hasVariantGroup) { Array.from(code.matchAll(regexClassGroup)) diff --git a/test/__snapshots__/preset-mini.test.ts.snap b/test/__snapshots__/preset-mini.test.ts.snap index 6b22715909..ff5793ef57 100644 --- a/test/__snapshots__/preset-mini.test.ts.snap +++ b/test/__snapshots__/preset-mini.test.ts.snap @@ -41,10 +41,16 @@ exports[`preset-mini > targets 1`] = ` .fw-\\\\$variable{font-weight:var(--variable);} .items-\\\\$size{align-items:var(--size);} .ws-\\\\$variable{white-space:var(--variable);} +.\\\\[--css-variable\\\\:\\\\\\"wght\\\\\\"_400\\\\,_\\\\\\"opsz\\\\\\"_14\\\\]{--css-variable:\\"wght\\" 400, \\"opsz\\" 14;} +.\\\\[--escaped\\\\\\\\\\\\~variable\\\\\\\\\\\\:\\\\:100\\\\%\\\\]{--escaped\\\\~variable\\\\::100%;} .\\\\[a\\\\:b\\\\]{a:b;} .\\\\[background-image\\\\:url\\\\(star_transparent\\\\.gif\\\\)\\\\,_url\\\\(cat_front\\\\.png\\\\)\\\\]{background-image:url(star_transparent.gif), url(cat_front.png);} .\\\\[content\\\\:attr\\\\(attr_content\\\\)\\\\]{content:attr(attr content);} .\\\\[content\\\\:attr\\\\(attr\\\\\\\\_content\\\\)\\\\]{content:attr(attr_content);} +.\\\\[font-family\\\\:\\\\'Inter\\\\'\\\\,_sans-serif\\\\]{font-family:'Inter', sans-serif;} +.\\\\[font-family\\\\:var\\\\(--font-family\\\\)\\\\]{font-family:var(--font-family);} +.\\\\[font-feature-settings\\\\:\\\\'cv02\\\\'\\\\,\\\\'cv03\\\\'\\\\,\\\\'cv04\\\\'\\\\,\\\\'cv11\\\\'\\\\]{font-feature-settings:'cv02','cv03','cv04','cv11';} +.\\\\[font-variation-settings\\\\:\\\\\\"wght\\\\\\"_400\\\\,_\\\\\\"opsz\\\\\\"_14\\\\]{font-variation-settings:\\"wght\\" 400, \\"opsz\\" 14;} .\\\\[margin\\\\:logical_1rem_2rem_3rem\\\\]{margin:logical 1rem 2rem 3rem;} .all-\\\\[\\\\.target\\\\]-\\\\[combinator\\\\:test-2\\\\] .target, .children-\\\\[\\\\.target\\\\]-\\\\[combinator\\\\:test-2\\\\]>.target, diff --git a/test/assets/preset-mini-targets.ts b/test/assets/preset-mini-targets.ts index 725e3629fc..1b37229f42 100644 --- a/test/assets/preset-mini-targets.ts +++ b/test/assets/preset-mini-targets.ts @@ -850,6 +850,12 @@ export const presetMiniTargets: string[] = [ '[content:attr(attr_content)]', '[content:attr(attr\\_content)]', '[background-image:url(star_transparent.gif),_url(cat_front.png)]', + '[font-family:var(--font-family)]', + '[font-family:\'Inter\',_sans-serif]', + '[font-feature-settings:\'cv02\',\'cv03\',\'cv04\',\'cv11\']', + '[font-variation-settings:"wght"_400,_"opsz"_14]', + '[--css-variable:"wght"_400,_"opsz"_14]', + '[--escaped\\~variable\\::100%]', // variants 'active:scale-4', @@ -1044,4 +1050,10 @@ export const presetMiniNonTargets = [ // variants - combinator 'all:[svg]:fill-red', + + // arbitrary css properties edge cases that cause invalid output + '[name].[hash:9]', + '["update:modelValue"]', + // escaped arbitrary css properties only allowed in css variables + '[cant\~escape:me]', ]