diff --git a/packages/preset-attributify/src/autocomplete.ts b/packages/preset-attributify/src/autocomplete.ts index a3ff955c88..a1c03d643f 100644 --- a/packages/preset-attributify/src/autocomplete.ts +++ b/packages/preset-attributify/src/autocomplete.ts @@ -1,94 +1,110 @@ import type { AutoCompleteExtractor } from '@unocss/core' import { variantsRE } from './variant' +import type { AttributifyOptions } from '.' const elementRE = /(<\w[\w:\.$-]*\s)((?:'[^>]*?'|"[^>]*?"|`[^>]*?`|\{[^>]*?\}|[^>]*?)*)/g const valuedAttributeRE = /([?]|(?!\d|-{2}|-\d)[a-zA-Z0-9\u00A0-\uFFFF-_:%-]+)(?:=("[^"]*|'[^']*))?/g const splitterRE = /[\s'"`;>]+/ -export const autocompleteExtractorAttributify: AutoCompleteExtractor = { - name: 'attributify', - extract: ({ content, cursor }) => { - const matchedElements = content.matchAll(elementRE) - let attrs: string | undefined - let elPos = 0 - for (const match of matchedElements) { - const [, prefix, content] = match - const currentPos = match.index! + prefix.length - if (cursor > currentPos && cursor <= currentPos + content.length) { - elPos = currentPos - attrs = content - break +export function autocompleteExtractorAttributify(options?: AttributifyOptions): AutoCompleteExtractor { + return { + name: 'attributify', + extract: ({ content, cursor }) => { + const matchedElements = content.matchAll(elementRE) + let attrs: string | undefined + let elPos = 0 + for (const match of matchedElements) { + const [, prefix, content] = match + const currentPos = match.index! + prefix.length + if (cursor > currentPos && cursor <= currentPos + content.length) { + elPos = currentPos + attrs = content + break + } } - } - if (!attrs) - return null + if (!attrs) + return null - const matchedAttributes = attrs.matchAll(valuedAttributeRE) - let attrsPos = 0 - let attrName: string | undefined - let attrValues: string | undefined - for (const match of matchedAttributes) { - const [matched, name, rawValues] = match - const currentPos = elPos + match.index! - if (cursor > currentPos && cursor <= currentPos + matched.length) { - attrsPos = currentPos - attrName = name - attrValues = rawValues?.slice(1) - break + const matchedAttributes = attrs.matchAll(valuedAttributeRE) + let attrsPos = 0 + let attrName: string | undefined + let attrValues: string | undefined + for (const match of matchedAttributes) { + const [matched, name, rawValues] = match + const currentPos = elPos + match.index! + if (cursor > currentPos && cursor <= currentPos + matched.length) { + attrsPos = currentPos + attrName = name + attrValues = rawValues?.slice(1) + break + } } - } - if (!attrName) - return null + if (!attrName) + return null - if (attrName === 'class' || attrName === 'className' || attrName === ':class') - return null - if (attrValues === undefined) { - return { - extracted: attrName, - resolveReplacement(suggestion) { - return { - start: attrsPos, - end: attrsPos + attrName!.length, - replacement: suggestion, - } - }, - } - } + if (attrName === 'class' || attrName === 'className' || attrName === ':class') + return null + + const hasPrefix = !!options?.prefix && attrName.startsWith(options.prefix) + if (options?.prefixedOnly && !hasPrefix) + return null - const attrValuePos = attrsPos + attrName.length + 2 + const attrNameWithoutPrefix = hasPrefix ? attrName.slice(options.prefix!.length) : attrName - let matchSplit = splitterRE.exec(attrValues) - let currentPos = 0 - let value: string | undefined - while (matchSplit) { - const [matched] = matchSplit - if (cursor > attrValuePos + currentPos - && cursor <= attrValuePos + currentPos + matchSplit.index) { - value = attrValues.slice(currentPos, currentPos + matchSplit.index) - break + if (attrValues === undefined) { + return { + extracted: attrNameWithoutPrefix, + transformSuggestions(suggestion) { + if (hasPrefix) + return suggestion.map(s => options.prefix! + s) + else + return suggestion + }, + resolveReplacement(suggestion) { + return { + start: attrsPos, + end: attrsPos + attrName!.length, + replacement: suggestion, + } + }, + } } - currentPos += matchSplit.index + matched.length - matchSplit = splitterRE.exec(attrValues.slice(currentPos)) - } - if (value === undefined) - value = attrValues.slice(currentPos) - const [, variants = '', body] = value.match(variantsRE) || [] + const attrValuePos = attrsPos + attrName.length + 2 - return { - extracted: `${variants}${attrName}-${body}`, - transformSuggestions(suggestions) { - return suggestions - .filter(v => v.startsWith(`${variants}${attrName}-`)) - .map(v => variants + v.slice(variants.length + attrName!.length + 1)) - }, - resolveReplacement(suggestion) { - return { - start: currentPos + attrValuePos, - end: currentPos + attrValuePos + value!.length, - replacement: variants + suggestion.slice(variants.length + attrName!.length + 1), + let matchSplit = splitterRE.exec(attrValues) + let currentPos = 0 + let value: string | undefined + while (matchSplit) { + const [matched] = matchSplit + if (cursor > attrValuePos + currentPos + && cursor <= attrValuePos + currentPos + matchSplit.index) { + value = attrValues.slice(currentPos, currentPos + matchSplit.index) + break } - }, - } - }, + currentPos += matchSplit.index + matched.length + matchSplit = splitterRE.exec(attrValues.slice(currentPos)) + } + if (value === undefined) + value = attrValues.slice(currentPos) + + const [, variants = '', body] = value.match(variantsRE) || [] + + return { + extracted: `${variants}${attrNameWithoutPrefix}-${body}`, + transformSuggestions(suggestions) { + return suggestions + .filter(v => v.startsWith(`${variants}${attrNameWithoutPrefix}-`)) + .map(v => variants + v.slice(variants.length + attrNameWithoutPrefix!.length + 1)) + }, + resolveReplacement(suggestion) { + return { + start: currentPos + attrValuePos, + end: currentPos + attrValuePos + value!.length, + replacement: variants + suggestion.slice(variants.length + attrNameWithoutPrefix!.length + 1), + } + }, + } + }, + } } diff --git a/packages/preset-attributify/src/index.ts b/packages/preset-attributify/src/index.ts index a04ea99688..1816cc048f 100644 --- a/packages/preset-attributify/src/index.ts +++ b/packages/preset-attributify/src/index.ts @@ -24,7 +24,7 @@ function presetAttributify(options: AttributifyOptions = {}): Preset { extractorAttributify(options), ] const autocompleteExtractors = [ - autocompleteExtractorAttributify, + autocompleteExtractorAttributify(options), ] return { diff --git a/test/preset-attributify.test.ts b/test/preset-attributify.test.ts index 56ea36c1bf..f70e17f7c4 100644 --- a/test/preset-attributify.test.ts +++ b/test/preset-attributify.test.ts @@ -76,34 +76,120 @@ describe('attributify', async () => { expect(css).toMatchSnapshot() }) - test('autocomplete extractor', async () => { - const res = await autocompleteExtractorAttributify.extract({ - content: fixture1, - cursor: 187, + describe('autocomplete extractor', async () => { + test('without prefix', async () => { + const res = await autocompleteExtractorAttributify().extract({ + content: fixture1, + cursor: 187, + }) + + expect(res).not.toBeNull() + + expect(res!.extracted).toMatchInlineSnapshot('"bg-blue-400"') + expect(res!.transformSuggestions!([`${res!.extracted}1`, `${res!.extracted}2`])) + .toMatchInlineSnapshot(` + [ + "blue-4001", + "blue-4002", + ] + `) + + const reversed = res!.resolveReplacement(`${res!.extracted}1`) + expect(reversed).toMatchInlineSnapshot(` + { + "end": 192, + "replacement": "blue-4001", + "start": 184, + } + `) + + expect(fixture1.slice(reversed.start, reversed.end)) + .toMatchInlineSnapshot('"blue-400"') }) - expect(res).not.toBeNull() + test('with prefix', async () => { + const fixtureWithPrefix = ` +
+
{ + const fixtureOnlyPrefix = ` +
+