Skip to content

Commit

Permalink
fix!: handle nested variant groups in vscode/playground and fix sourc…
Browse files Browse the repository at this point in the history
…emaps (#2890)

Co-authored-by: Anthony Fu <anthonyfu117@hotmail.com>
  • Loading branch information
russelldavis and antfu committed Jul 23, 2023
1 parent 995cc62 commit 9efa545
Show file tree
Hide file tree
Showing 13 changed files with 446 additions and 112 deletions.
16 changes: 9 additions & 7 deletions packages/core/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -619,12 +619,10 @@ export interface SourceMap {
version?: number
}

export interface TransformResult {
code: string
map?: SourceMap | null
etag?: string
deps?: string[]
dynamicDeps?: string[]
export interface HighlightAnnotation {
offset: number
length: number
className: string
}

export type SourceCodeTransformerEnforce = 'pre' | 'post' | 'default'
Expand All @@ -642,7 +640,11 @@ export interface SourceCodeTransformer {
/**
* The transform function
*/
transform: (code: MagicString, id: string, ctx: UnocssPluginContext) => Awaitable<void>
transform: (
code: MagicString,
id: string,
ctx: UnocssPluginContext
) => Awaitable<{ highlightAnnotations?: HighlightAnnotation[] } | void>
}

export interface ContentOptions {
Expand Down
81 changes: 57 additions & 24 deletions packages/core/src/utils/variantGroup.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import type MagicString from 'magic-string'
import MagicString from 'magic-string'
import type { HighlightAnnotation } from '../types'
import { notNull } from '../utils'

const regexCache: Record<string, RegExp> = {}
Expand All @@ -11,37 +12,77 @@ export function makeRegexClassGroup(separators = ['-', ':']) {
return regexCache[key]
}

export function parseVariantGroup(str: string, separators = ['-', ':'], depth = 5) {
interface VariantGroup {
length: number
items: HighlightAnnotation[]
}

export function parseVariantGroup(str: string | MagicString, separators = ['-', ':'], depth = 5) {
const regexClassGroup = makeRegexClassGroup(separators)
let hasChanged = false
let hasChanged
let content = str.toString()
const prefixes = new Set<string>()
const groupsByOffset = new Map<number, VariantGroup>()

do {
const before = content
hasChanged = false
content = content.replace(
regexClassGroup,
(from, pre, sep, body: string) => {
(from, pre: string, sep: string, body: string, groupOffset: number) => {
if (!separators.includes(sep))
return from

hasChanged = true
prefixes.add(pre + sep)
const bodyOffset = groupOffset + pre.length + sep.length + 1
const group: VariantGroup = { length: from.length, items: [] }
groupsByOffset.set(groupOffset, group)

return body
.split(/\s/g)
.filter(Boolean)
.map(i => i === '~' ? pre : i.replace(/^(!?)(.*)/, `$1${pre}${sep}$2`))
.join(' ')
for (const itemMatch of [...body.matchAll(/\S+/g)]) {
const itemOffset = bodyOffset + itemMatch.index!
let innerItems = groupsByOffset.get(itemOffset)?.items
if (innerItems) {
// We won't need to look up this group from this offset again.
// It gets added to the current group below.
groupsByOffset.delete(itemOffset)
}
else {
innerItems = [{
offset: itemOffset,
length: itemMatch[0].length,
className: itemMatch[0],
}]
}
for (const item of innerItems) {
item.className = item.className === '~'
? pre
: item.className.replace(/^(!?)(.*)/, `$1${pre}${sep}$2`)
group.items.push(item)
}
}
// The replacement string just needs to be the same length (so it doesn't mess up offsets)
// and not contain any grouping/separator characters (so any outer groups will match on
// the next pass). The final value of `content` won't be used; we construct the final result
// below using groupsByOffset.
return '$'.repeat(from.length)
},
)
hasChanged = content !== before
depth -= 1
} while (hasChanged && depth)

const expanded = typeof str === 'string'
? new MagicString(str)
: str

for (const [offset, group] of groupsByOffset)
expanded.overwrite(offset, offset + group.length, group.items.map(item => item.className).join(' '))

return {
prefixes: Array.from(prefixes),
expanded: content,
hasChanged,
groupsByOffset,
// Computed lazily because MagicString's toString does a lot of work
get expanded() { return expanded.toString() },
}
}

Expand Down Expand Up @@ -81,16 +122,8 @@ export function collapseVariantGroup(str: string, prefixes: string[]): string {
export function expandVariantGroup(str: string, separators?: string[], depth?: number): string
export function expandVariantGroup(str: MagicString, separators?: string[], depth?: number): MagicString
export function expandVariantGroup(str: string | MagicString, separators = ['-', ':'], depth = 5) {
const {
expanded,
} = parseVariantGroup(str.toString(), separators, depth)

if (typeof str === 'string') {
return expanded
}
else {
return str.length()
? str.overwrite(0, str.original.length, expanded)
: str
}
const res = parseVariantGroup(str, separators, depth)
return typeof str === 'string'
? res.expanded
: str
}
7 changes: 5 additions & 2 deletions packages/inspector/client/components/CodeMirror.vue
Original file line number Diff line number Diff line change
@@ -1,15 +1,18 @@
<script setup lang="ts">
import { getMatchedPositions } from '@unocss/shared-common'
import type { HighlightAnnotation } from '@unocss/core'
import { Decoration } from '@codemirror/view'
import { useEventListener, useThrottleFn } from '@vueuse/core'
import { useEventListener, useThrottleFn, useVModel } from '@vueuse/core'
import type { CompletionSource } from '@codemirror/autocomplete'
import { onMounted, reactive, ref, toRefs, watch } from 'vue'
import { addMarks, filterMarks, useCodeMirror } from '../composables/codemirror'
const props = defineProps<{
modelValue: string
mode?: string
readOnly?: boolean
matched?: Set<string> | string[]
annotations?: HighlightAnnotation[]
getHint?: CompletionSource
}>()
Expand Down Expand Up @@ -41,7 +44,7 @@ onMounted(async () => {
cm.dispatch({
effects: filterMarks.of((from: number, to: number) => to <= 0 || from >= cm.state.doc.toString().length),
})
getMatchedPositions(props.modelValue, Array.from(props.matched || []), true)
getMatchedPositions(props.modelValue, Array.from(props.matched || []), props.annotations || [])
.forEach(i => mark(i[0], i[1]))
}
Expand Down
1 change: 1 addition & 0 deletions packages/reset/package.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
{
"name": "@unocss/reset",
"type": "module",
"version": "0.53.6",
"description": "Collection of CSS resetting",
"author": "Anthony Fu <anthonyfu117@hotmail.com>",
Expand Down
14 changes: 9 additions & 5 deletions packages/reset/scripts/copy.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,17 @@
import fs from 'node:fs'
import fs from 'node:fs/promises'
import { existsSync } from 'node:fs'
import { dirname } from 'node:path'
import { createRequire } from 'node:module'

fs.copyFileSync(
const require = createRequire(import.meta.url)

await fs.copyFile(
require.resolve('@csstools/normalize.css'),
'normalize.css',
)

if (!fs.existsSync('sanitize'))
fs.mkdirSync('sanitize')
if (!existsSync('sanitize'))
await fs.mkdir('sanitize')

for (const stylesheet of [
'sanitize.css',
Expand All @@ -18,7 +22,7 @@ for (const stylesheet of [
'system-ui.css',
'ui-monospace.css',
]) {
fs.copyFileSync(
await fs.copyFile(
`${dirname(require.resolve('sanitize.css'))}/${stylesheet}`,
`sanitize/${stylesheet}`,
)
Expand Down
58 changes: 23 additions & 35 deletions packages/shared-common/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import type { ExtractorContext, UnoGenerator } from '@unocss/core'
import { escapeRegExp, isAttributifySelector, makeRegexClassGroup, splitWithVariantGroupRE } from '@unocss/core'
import type { ExtractorContext, HighlightAnnotation, UnoGenerator } from '@unocss/core'
import { escapeRegExp, isAttributifySelector, splitWithVariantGroupRE } from '@unocss/core'
import MagicString from 'magic-string'
import { arbitraryPropertyRE, quotedArbitraryValuesRE } from '../../extractor-arbitrary-variants/src'

Expand Down Expand Up @@ -85,8 +85,13 @@ export function getPlainClassMatchedPositionsForPug(codeSplit: string, matchedPl
return result
}

export function getMatchedPositions(code: string, matched: string[], hasVariantGroup = false, isPug = false, uno: UnoGenerator | undefined = undefined) {
const result: [number, number, string][] = []
export function getMatchedPositions(
code: string,
matched: string[],
extraAnnotations: HighlightAnnotation[] = [],
isPug = false,
) {
const result: (readonly [start: number, end: number, text: string])[] = []
const attributify: RegExpMatchArray[] = []
const plain = new Set<string>()

Expand Down Expand Up @@ -149,29 +154,6 @@ export function getMatchedPositions(code: string, matched: string[], hasVariantG
}
}

// highlight for variant group
if (hasVariantGroup) {
Array.from(code.matchAll(makeRegexClassGroup(uno?.config.separators)))
.forEach((match) => {
const [, pre, sep, body] = match
const index = match.index!
let start = index + pre.length + sep.length + 1
body.split(/([\s"'`;*]|:\(|\)"|\)\s)/g).forEach((i) => {
const end = start + i.length
const full = pre + sep + i
if (plain.has(full)) {
// find existing plain class match and replace it
const index = result.findIndex(([s, e]) => s === start && e === end)
if (index < 0)
result.push([start, end, full])
else
result[index][2] = full
}
start = end
})
})
}

// attributify values
attributify.forEach(([, name, value]) => {
const regex = new RegExp(`(${escapeRegExp(name)}=)(['"])[^\\2]*?${escapeRegExp(value)}[^\\2]*?\\2`, 'g')
Expand All @@ -190,6 +172,10 @@ export function getMatchedPositions(code: string, matched: string[], hasVariantG
})
})

result.push(...extraAnnotations.map(i =>
[i.offset, i.offset + i.length, i.className] as const,
))

return result.sort((a, b) => a[0] - b[0])
}

Expand All @@ -205,15 +191,17 @@ export async function getMatchedPositionsFromCode(uno: UnoGenerator, code: strin
const ctx = { uno, tokens } as any

const transformers = uno.config.transformers?.filter(i => !ignoreTransformers.includes(i.name))
for (const i of transformers?.filter(i => i.enforce === 'pre') || [])
await i.transform(s, id, ctx)
for (const i of transformers?.filter(i => !i.enforce || i.enforce === 'default') || [])
await i.transform(s, id, ctx)
for (const i of transformers?.filter(i => i.enforce === 'post') || [])
await i.transform(s, id, ctx)
const hasVariantGroup = !!uno.config.transformers?.find(i => i.name === '@unocss/transformer-variant-group')
const annotations = []
for (const enforce of ['pre', 'default', 'post']) {
for (const i of transformers?.filter(i => (i.enforce ?? 'default') === enforce) || []) {
const result = await i.transform(s, id, ctx)
const _annotations = result?.highlightAnnotations
if (_annotations)
annotations.push(..._annotations)
}
}

const { pug, code: pugCode } = await isPug(uno, s.toString(), id)
const result = await uno.generate(pug ? pugCode : s.toString(), { preflights: false })
return getMatchedPositions(code, [...result.matched], hasVariantGroup, pug, uno)
return getMatchedPositions(code, [...result.matched], annotations, pug)
}
13 changes: 10 additions & 3 deletions packages/transformer-variant-group/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import type { SourceCodeTransformer } from '@unocss/core'
import { expandVariantGroup } from '@unocss/core'
import { parseVariantGroup } from '@unocss/core'

export interface TransformerVariantGroupOptions {
/**
Expand All @@ -19,12 +19,19 @@ export interface TransformerVariantGroupOptions {
separators?: (':' | '-')[]
}

export default function transformerVariantGroup(options: TransformerVariantGroupOptions = {}): SourceCodeTransformer {
export default function transformerVariantGroup(
options: TransformerVariantGroupOptions = {},
): SourceCodeTransformer {
return {
name: '@unocss/transformer-variant-group',
enforce: 'pre',
transform(s) {
expandVariantGroup(s, options.separators)
const result = parseVariantGroup(s, options.separators)
return {
get highlightAnnotations() {
return [...result.groupsByOffset.values()].flatMap(group => group.items)
},
}
},
}
}
4 changes: 4 additions & 0 deletions playground/src/auto-imports.d.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
/* eslint-disable */
/* prettier-ignore */
// @ts-nocheck
// noinspection JSUnusedGlobalSymbols
// Generated by unplugin-auto-import
export {}
declare global {
const EffectScope: typeof import('vue')['EffectScope']
const STORAGE_KEY: typeof import('./composables/constants')['STORAGE_KEY']
const annotations: typeof import('./composables/uno')['annotations']
const asyncComputed: typeof import('@vueuse/core')['asyncComputed']
const autoResetRef: typeof import('@vueuse/core')['autoResetRef']
const computed: typeof import('vue')['computed']
Expand Down Expand Up @@ -340,6 +342,7 @@ declare module 'vue' {
interface ComponentCustomProperties {
readonly EffectScope: UnwrapRef<typeof import('vue')['EffectScope']>
readonly STORAGE_KEY: UnwrapRef<typeof import('./composables/constants')['STORAGE_KEY']>
readonly annotations: UnwrapRef<typeof import('./composables/uno')['annotations']>
readonly asyncComputed: UnwrapRef<typeof import('@vueuse/core')['asyncComputed']>
readonly autoResetRef: UnwrapRef<typeof import('@vueuse/core')['autoResetRef']>
readonly computed: UnwrapRef<typeof import('vue')['computed']>
Expand Down Expand Up @@ -668,6 +671,7 @@ declare module '@vue/runtime-core' {
interface ComponentCustomProperties {
readonly EffectScope: UnwrapRef<typeof import('vue')['EffectScope']>
readonly STORAGE_KEY: UnwrapRef<typeof import('./composables/constants')['STORAGE_KEY']>
readonly annotations: UnwrapRef<typeof import('./composables/uno')['annotations']>
readonly asyncComputed: UnwrapRef<typeof import('@vueuse/core')['asyncComputed']>
readonly autoResetRef: UnwrapRef<typeof import('@vueuse/core')['autoResetRef']>
readonly computed: UnwrapRef<typeof import('vue')['computed']>
Expand Down
4 changes: 1 addition & 3 deletions playground/src/components.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,9 @@
// @ts-nocheck
// Generated by unplugin-vue-components
// Read more: https://github.com/vuejs/core/pull/3399
import '@vue/runtime-core'

export {}

declare module '@vue/runtime-core' {
declare module 'vue' {
export interface GlobalComponents {
CodeMirror: typeof import('./../../packages/inspector/client/components/CodeMirror.vue')['default']
Editor: typeof import('./components/Editor.vue')['default']
Expand Down
1 change: 1 addition & 0 deletions playground/src/components/panel/PanelHtml.vue
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ const computedInputHTML = computed({
mode="html"
class="scrolls border-(l gray-400/20)"
:matched="output?.matched || new Set()"
:annotations="annotations"
:get-hint="getHint"
:read-only="options.transform"
/>
Expand Down

0 comments on commit 9efa545

Please sign in to comment.