Skip to content

Commit

Permalink
feat(vscode): support annotation for pug (#1881)
Browse files Browse the repository at this point in the history
Co-authored-by: Anthony Fu <anthonyfu117@hotmail.com>
  • Loading branch information
azaleta and antfu committed Nov 29, 2022
1 parent d1b2582 commit d9d1b21
Show file tree
Hide file tree
Showing 3 changed files with 287 additions and 7 deletions.
72 changes: 66 additions & 6 deletions 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'

Expand Down Expand Up @@ -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<string>, 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<string>()
Expand All @@ -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
})

Expand Down Expand Up @@ -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)
}

91 changes: 91 additions & 0 deletions 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",
],
]
`;
131 changes: 130 additions & 1 deletion 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({
Expand Down Expand Up @@ -139,3 +142,129 @@ describe('matched-positions', async () => {
})
})

describe('matched-positions-pug', async () => {
const matchPug = (uno: UnoGenerator, code: string) => {
return match(uno,
`<template lang='pug'>
${code}
</template>`, '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",
],
]
`)
})
})

0 comments on commit d9d1b21

Please sign in to comment.