diff --git a/packages/shared-common/src/index.ts b/packages/shared-common/src/index.ts index 8d60f1690f..cb221038e4 100644 --- a/packages/shared-common/src/index.ts +++ b/packages/shared-common/src/index.ts @@ -1,4 +1,4 @@ -import type { UnoGenerator } from '@unocss/core' +import type { ExtractorContext, UnoGenerator } from '@unocss/core' import { arbitraryPropertyRE, escapeRegExp, isAttributifySelector, regexClassGroup } from '@unocss/core' import MagicString from 'magic-string' @@ -28,7 +28,59 @@ export function replaceAsync(string: string, searchValue: RegExp, replacer: (... } } -export function getMatchedPositions(code: string, matched: string[], hasVariantGroup = false) { +export async function isPug(uno: UnoGenerator, code: string, id = '') { + const pugExtractor = uno.config.extractors?.find(e => e.name === 'pug') + if (!pugExtractor) + return { pug: false, code: '' } + + const ctx = { code, id } as ExtractorContext + await pugExtractor.extract(ctx) + const extractResult = ctx.code.startsWith(code) ? ctx.code.substring(code.length + 2) : ctx.code + return ctx.code !== code ? { pug: true, code: extractResult } : { pug: false, code: '' } +} + +export function getPlainClassMatchedPositionsForPug(codeSplit: string, matchedPlain: Set, start: number) { + const result: [number, number, string][] = [] + matchedPlain.forEach((plainClassName) => { + // normal case: match for 'p1' + // end with EOL : div.p1 + // end with . : div.p1.ma + // end with # : div.p1#id + // end with = : div.p1= content + // end with space : div.p1 content + // end with ( : div.p1(text="red") + + // complex case: match for hover:scale-100 + // such as [div.hover:scale-100] will not be parsed correctly by pug + // should use [div(class='hover:scale-100')] + + // combine both cases will be 2 syntax + // div.p1(class='hover:scale-100') + // div(class='hover:scale-100 p1') -> p1 should be parsing as well + if (plainClassName.includes(':')) { + if (plainClassName === codeSplit) + result.push([start, start + plainClassName.length, plainClassName]) + } + else { + const regex = new RegExp(`\.(${plainClassName})[\.#=\s(]|\.(${plainClassName})$`) + const match = regex.exec(codeSplit) + if (match) { + // keep [.] not include -> .p1 will only show underline on [p1] + result.push([start + match.index + 1, start + match.index + plainClassName.length + 1, plainClassName]) + } + else { + // div(class='hover:scale-100 p1') -> parsing p1 + // this will only be triggered if normal case fails + if (plainClassName === codeSplit) + result.push([start, start + plainClassName.length, plainClassName]) + } + } + }) + + return result +} + +export function getMatchedPositions(code: string, matched: string[], hasVariantGroup = false, isPug = false) { const result: [number, number, string][] = [] const attributify: RegExpMatchArray[] = [] const plain = new Set() @@ -48,8 +100,13 @@ export function getMatchedPositions(code: string, matched: string[], hasVariantG let start = 0 code.split(/([\s"'`;<>]|:\(|\)"|\)\s)/g).forEach((i) => { const end = start + i.length - if (plain.has(i)) - result.push([start, end, i]) + if (isPug) { + result.push(...getPlainClassMatchedPositionsForPug(i, plain, start)) + } + else { + if (plain.has(i)) + result.push([start, end, i]) + } start = end }) @@ -122,6 +179,9 @@ export async function getMatchedPositionsFromCode(uno: UnoGenerator, code: strin 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 === 'variant-group') - const result = await uno.generate(s.toString(), { preflights: false }) - return getMatchedPositions(code, [...result.matched], hasVariantGroup) + + 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) } + diff --git a/test/__snapshots__/pos.test.ts.snap b/test/__snapshots__/pos.test.ts.snap new file mode 100644 index 0000000000..0009f957ec --- /dev/null +++ b/test/__snapshots__/pos.test.ts.snap @@ -0,0 +1,91 @@ +// Vitest Snapshot v1 + +exports[`matched-positions-pug > plain class: normal case 1`] = ` +[ + [ + 28, + 30, + "p1", + ], + [ + 31, + 33, + "ma", + ], + [ + 44, + 46, + "p2", + ], + [ + 61, + 63, + "p4", + ], + [ + 64, + 66, + "p5", + ], + [ + 81, + 83, + "p7", + ], + [ + 97, + 99, + "p9", + ], + [ + 106, + 109, + "[text=\\"red\\"]", + ], +] +`; + +exports[`matched-positions-pug > plain class: prefix 1`] = ` +[ + [ + 35, + 50, + "hover:scale-100", + ], + [ + 68, + 82, + "hover:scale-90", + ], + [ + 100, + 114, + "hover:scale-80", + ], + [ + 115, + 117, + "p1", + ], + [ + 135, + 149, + "hover:scale-70", + ], + [ + 150, + 152, + "p2", + ], + [ + 164, + 166, + "p3", + ], + [ + 174, + 188, + "hover:scale-60", + ], +] +`; diff --git a/test/pos.test.ts b/test/pos.test.ts index c527f867d7..61b8dca4c3 100644 --- a/test/pos.test.ts +++ b/test/pos.test.ts @@ -1,11 +1,14 @@ import { describe, expect, test } from 'vitest' import presetAttributify from '@unocss/preset-attributify' import presetUno from '@unocss/preset-uno' -import { createGenerator } from '@unocss/core' +import type { UnoGenerator } from '@unocss/core' +import { createGenerator, extractorSplit } from '@unocss/core' import { getMatchedPositionsFromCode as match } from '@unocss/shared-common' import transformerVariantGroup from '@unocss/transformer-variant-group' import cssDirectives from '@unocss/transformer-directives' +import extractorPug from '@unocss/extractor-pug' + describe('matched-positions', async () => { test('attributify', async () => { const uno = createGenerator({ @@ -139,3 +142,129 @@ describe('matched-positions', async () => { }) }) +describe('matched-positions-pug', async () => { + const matchPug = (uno: UnoGenerator, code: string) => { + return match(uno, +``, 'App.vue') + } + + const uno = createGenerator({ + presets: [ + presetUno(), + presetAttributify({ strict: true }), + ], + extractors: [ + extractorSplit, + extractorPug(), + ], + transformers: [ + transformerVariantGroup(), + ], + }) + + test('plain class: normal case', async () => { + const pugCode = `div.p1.ma + div.p2#id1 + div.p4.p5= p6 + div.p7 p8 + div.p9(text="red") + ` + expect(await matchPug(uno, pugCode)).toMatchSnapshot() + }, 20000) + + test('plain class: prefix', async () => { + const pugCode = `div(class='hover:scale-100') + div(class="hover:scale-90") + div(class="hover:scale-80 p1") + div(class="hover:scale-70 p2 ") + div.p3(class="hover:scale-60") + ` + expect(await matchPug(uno, pugCode)).toMatchSnapshot() + }, 20000) + + test('attributify', async () => { + const pugCode = `div.p4(border="b gray4") + div(text='red') + ` + expect(await matchPug(uno, pugCode)).toMatchInlineSnapshot(` + [ + [ + 28, + 30, + "p4", + ], + [ + 39, + 40, + "b", + ], + [ + 39, + 40, + "[border=\\"b\\"]", + ], + [ + 41, + 46, + "[border=\\"gray4\\"]", + ], + [ + 65, + 68, + "[text=\\"red\\"]", + ], + ] + `) + }) + + test('variant group', async () => { + const pugCode = 'div.p4(class="hover:(h-4 w-4)")' + expect(await matchPug(uno, pugCode)).toMatchInlineSnapshot(` + [ + [ + 28, + 30, + "p4", + ], + [ + 45, + 48, + "hover:h-4", + ], + [ + 49, + 52, + "hover:w-4", + ], + ] + `) + }) + + test('css-directive', async () => { + // \n could not be include + // div.p2(class="btn-center{@apply p1 m1;\n}") -> pug parse error + const pugCode = 'div.p2(class="btn-center{@apply p1 m1;}")' + expect(await matchPug(uno, pugCode)).toMatchInlineSnapshot(` + [ + [ + 28, + 30, + "p2", + ], + [ + 56, + 58, + "p1", + ], + [ + 59, + 61, + "m1", + ], + ] + `) + }) +}) +