tailwindcss-patch 9.0.1 → 9.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,224 @@
1
+ import type { TailwindV4DesignSystem } from './types'
2
+ import postcss from 'postcss'
3
+
4
+ export function resolveValidTailwindV4Candidates(
5
+ designSystem: TailwindV4DesignSystem,
6
+ candidates: Iterable<string>,
7
+ ): Set<string> {
8
+ const validCandidates = new Set<string>()
9
+ const parsedCandidates: string[] = []
10
+
11
+ for (const candidate of candidates) {
12
+ if (!candidate || parsedCandidates.includes(candidate)) {
13
+ continue
14
+ }
15
+
16
+ if (designSystem.parseCandidate(candidate).length > 0) {
17
+ parsedCandidates.push(candidate)
18
+ }
19
+ }
20
+
21
+ if (parsedCandidates.length === 0) {
22
+ return validCandidates
23
+ }
24
+
25
+ const cssByCandidate = designSystem.candidatesToCss(parsedCandidates)
26
+ for (let index = 0; index < parsedCandidates.length; index++) {
27
+ const candidate = parsedCandidates[index]
28
+ const candidateCss = cssByCandidate[index]
29
+ if (candidate && typeof candidateCss === 'string' && candidateCss.trim().length > 0) {
30
+ validCandidates.add(candidate)
31
+ }
32
+ }
33
+
34
+ return validCandidates
35
+ }
36
+
37
+ function splitTopLevel(value: string, separator: string) {
38
+ const result: string[] = []
39
+ let start = 0
40
+ let depth = 0
41
+ let quote: string | undefined
42
+
43
+ for (let index = 0; index < value.length; index++) {
44
+ const character = value[index]
45
+ if (character === '\\') {
46
+ index++
47
+ continue
48
+ }
49
+
50
+ if (quote) {
51
+ if (character === quote) {
52
+ quote = undefined
53
+ }
54
+ continue
55
+ }
56
+
57
+ if (character === '"' || character === '\'') {
58
+ quote = character
59
+ continue
60
+ }
61
+
62
+ if (character === '(' || character === '[' || character === '{') {
63
+ depth++
64
+ continue
65
+ }
66
+
67
+ if (character === ')' || character === ']' || character === '}') {
68
+ depth = Math.max(0, depth - 1)
69
+ continue
70
+ }
71
+
72
+ if (depth === 0 && character === separator) {
73
+ const item = value.slice(start, index).trim()
74
+ if (item) {
75
+ result.push(item)
76
+ }
77
+ start = index + 1
78
+ }
79
+ }
80
+
81
+ const item = value.slice(start).trim()
82
+ if (item) {
83
+ result.push(item)
84
+ }
85
+ return result
86
+ }
87
+
88
+ const sequencePattern = /^(-?\d+)\.\.(-?\d+)(?:\.\.(-?\d+))?$/
89
+
90
+ function expandSequence(value: string) {
91
+ const match = value.match(sequencePattern)
92
+ if (!match) {
93
+ return [value]
94
+ }
95
+
96
+ const [, startValue, endValue, stepValue] = match
97
+ if (startValue === undefined || endValue === undefined) {
98
+ return [value]
99
+ }
100
+
101
+ const start = Number.parseInt(startValue, 10)
102
+ const end = Number.parseInt(endValue, 10)
103
+ let step = stepValue === undefined ? (start <= end ? 1 : -1) : Number.parseInt(stepValue, 10)
104
+ if (step === 0) {
105
+ throw new Error('Step cannot be zero in Tailwind CSS v4 inline source sequence.')
106
+ }
107
+
108
+ const ascending = start < end
109
+ if (ascending && step < 0) {
110
+ step = -step
111
+ }
112
+ if (!ascending && step > 0) {
113
+ step = -step
114
+ }
115
+
116
+ const result: string[] = []
117
+ for (let current = start; ascending ? current <= end : current >= end; current += step) {
118
+ result.push(current.toString())
119
+ }
120
+ return result
121
+ }
122
+
123
+ function expandInlinePattern(pattern: string): string[] {
124
+ const openIndex = pattern.indexOf('{')
125
+ if (openIndex === -1) {
126
+ return [pattern]
127
+ }
128
+
129
+ const prefix = pattern.slice(0, openIndex)
130
+ const rest = pattern.slice(openIndex)
131
+ let depth = 0
132
+ let closeIndex = -1
133
+ for (let index = 0; index < rest.length; index++) {
134
+ const character = rest[index]
135
+ if (character === '{') {
136
+ depth++
137
+ }
138
+ else if (character === '}') {
139
+ depth--
140
+ if (depth === 0) {
141
+ closeIndex = index
142
+ break
143
+ }
144
+ }
145
+ }
146
+
147
+ if (closeIndex === -1) {
148
+ throw new Error(`The Tailwind CSS v4 inline source pattern "${pattern}" is not balanced.`)
149
+ }
150
+
151
+ const body = rest.slice(1, closeIndex)
152
+ const suffix = rest.slice(closeIndex + 1)
153
+ const parts = sequencePattern.test(body)
154
+ ? expandSequence(body)
155
+ : splitTopLevel(body, ',').flatMap(part => expandInlinePattern(part))
156
+ const suffixes = expandInlinePattern(suffix)
157
+
158
+ const result: string[] = []
159
+ for (const part of parts) {
160
+ for (const expandedSuffix of suffixes) {
161
+ result.push(`${prefix}${part}${expandedSuffix}`)
162
+ }
163
+ }
164
+ return result
165
+ }
166
+
167
+ function unquoteCssString(value: string) {
168
+ const quote = value[0]
169
+ if ((quote !== '"' && quote !== '\'') || value[value.length - 1] !== quote) {
170
+ return undefined
171
+ }
172
+
173
+ let result = ''
174
+ for (let index = 1; index < value.length - 1; index++) {
175
+ const character = value[index]
176
+ if (character === '\\') {
177
+ index++
178
+ result += value[index] ?? ''
179
+ continue
180
+ }
181
+ result += character
182
+ }
183
+ return result
184
+ }
185
+
186
+ export function extractTailwindV4InlineSourceCandidates(css: string) {
187
+ const included = new Set<string>()
188
+ const excluded = new Set<string>()
189
+
190
+ const root = postcss.parse(css)
191
+ root.walkAtRules('source', (rule) => {
192
+ let params = rule.params.trim()
193
+ if (!params) {
194
+ return
195
+ }
196
+
197
+ let negated = false
198
+ if (params.startsWith('not ')) {
199
+ negated = true
200
+ params = params.slice(4).trim()
201
+ }
202
+
203
+ if (!params.startsWith('inline(') || !params.endsWith(')')) {
204
+ return
205
+ }
206
+
207
+ const inlineValue = unquoteCssString(params.slice(7, -1).trim())
208
+ if (inlineValue === undefined) {
209
+ return
210
+ }
211
+
212
+ const target = negated ? excluded : included
213
+ for (const part of splitTopLevel(inlineValue, ' ')) {
214
+ for (const candidate of expandInlinePattern(part)) {
215
+ target.add(candidate)
216
+ }
217
+ }
218
+ })
219
+
220
+ return {
221
+ included,
222
+ excluded,
223
+ }
224
+ }
@@ -0,0 +1,70 @@
1
+ import type {
2
+ TailwindV4Engine,
3
+ TailwindV4GenerateOptions,
4
+ TailwindV4GenerateResult,
5
+ TailwindV4ResolvedSource,
6
+ } from './types'
7
+ import { extractRawCandidatesWithPositions } from '../extraction/candidate-extractor'
8
+ import { extractTailwindV4InlineSourceCandidates, resolveValidTailwindV4Candidates } from './candidates'
9
+ import { compileTailwindV4Source, loadTailwindV4DesignSystem } from './node-adapter'
10
+
11
+ async function collectRawCandidates(source: TailwindV4ResolvedSource, options: TailwindV4GenerateOptions | undefined) {
12
+ const rawCandidates = new Set<string>()
13
+
14
+ for (const candidate of options?.candidates ?? []) {
15
+ rawCandidates.add(candidate)
16
+ }
17
+
18
+ for (const candidateSource of options?.sources ?? []) {
19
+ const candidates = await extractRawCandidatesWithPositions(candidateSource.content, candidateSource.extension)
20
+ for (const candidate of candidates) {
21
+ rawCandidates.add(candidate.rawCandidate)
22
+ }
23
+ }
24
+
25
+ const inlineSources = extractTailwindV4InlineSourceCandidates(source.css)
26
+ for (const candidate of inlineSources.included) {
27
+ rawCandidates.add(candidate)
28
+ }
29
+ for (const candidate of inlineSources.excluded) {
30
+ rawCandidates.delete(candidate)
31
+ }
32
+
33
+ return rawCandidates
34
+ }
35
+
36
+ export function createTailwindV4Engine(source: TailwindV4ResolvedSource): TailwindV4Engine {
37
+ return {
38
+ source,
39
+ loadDesignSystem() {
40
+ return loadTailwindV4DesignSystem(source)
41
+ },
42
+ async validateCandidates(candidates) {
43
+ const designSystem = await loadTailwindV4DesignSystem(source)
44
+ return resolveValidTailwindV4Candidates(designSystem, candidates)
45
+ },
46
+ async generate(options): Promise<TailwindV4GenerateResult> {
47
+ const rawCandidates = await collectRawCandidates(source, options)
48
+ const designSystem = await loadTailwindV4DesignSystem(source)
49
+ const classSet = resolveValidTailwindV4Candidates(designSystem, rawCandidates)
50
+ const inlineSources = extractTailwindV4InlineSourceCandidates(source.css)
51
+ // TODO: Non-inline `@source not "..."` is surfaced through `compiled.sources`;
52
+ // apply it here if generate() grows filesystem SourceEntry scanning.
53
+ for (const candidate of inlineSources.excluded) {
54
+ classSet.delete(candidate)
55
+ }
56
+
57
+ const { compiled, dependencies } = await compileTailwindV4Source(source)
58
+ const css = compiled.build(Array.from(classSet))
59
+
60
+ return {
61
+ css,
62
+ classSet,
63
+ rawCandidates,
64
+ dependencies: Array.from(dependencies),
65
+ sources: compiled.sources,
66
+ root: compiled.root,
67
+ }
68
+ },
69
+ }
70
+ }
@@ -0,0 +1,25 @@
1
+ export {
2
+ extractTailwindV4InlineSourceCandidates,
3
+ resolveValidTailwindV4Candidates,
4
+ } from './candidates'
5
+ export { createTailwindV4Engine } from './engine'
6
+ export {
7
+ compileTailwindV4Source,
8
+ loadTailwindV4DesignSystem,
9
+ loadTailwindV4NodeModule,
10
+ } from './node-adapter'
11
+ export {
12
+ resolveTailwindV4Source,
13
+ resolveTailwindV4SourceFromPatchOptions,
14
+ tailwindV4SourceOptionsFromPatchOptions,
15
+ } from './source'
16
+ export type {
17
+ TailwindV4CandidateSource,
18
+ TailwindV4DesignSystem,
19
+ TailwindV4Engine,
20
+ TailwindV4GenerateOptions,
21
+ TailwindV4GenerateResult,
22
+ TailwindV4ResolvedSource,
23
+ TailwindV4SourceOptions,
24
+ TailwindV4SourcePattern,
25
+ } from './types'
@@ -0,0 +1,149 @@
1
+ import type {
2
+ TailwindV4DesignSystem,
3
+ TailwindV4ResolvedSource,
4
+ TailwindV4SourcePattern,
5
+ } from './types'
6
+ import { createRequire } from 'node:module'
7
+ import { pathToFileURL } from 'node:url'
8
+ import path from 'pathe'
9
+
10
+ interface TailwindV4CompiledSource {
11
+ sources: TailwindV4SourcePattern[]
12
+ root: null | 'none' | {
13
+ base: string
14
+ pattern: string
15
+ }
16
+ build: (candidates: string[]) => string
17
+ }
18
+
19
+ interface TailwindV4NodeModule {
20
+ compile: (css: string, options: {
21
+ base: string
22
+ onDependency: (dependency: string) => void
23
+ }) => Promise<TailwindV4CompiledSource>
24
+ __unstable__loadDesignSystem: (css: string, options: { base: string }) => Promise<TailwindV4DesignSystem>
25
+ }
26
+
27
+ const nodeModulePromiseCache = new Map<string, Promise<TailwindV4NodeModule>>()
28
+ const designSystemPromiseCache = new Map<string, Promise<TailwindV4DesignSystem>>()
29
+
30
+ function unique(values: Iterable<string>) {
31
+ return Array.from(new Set(Array.from(values).filter(Boolean).map(value => path.resolve(value))))
32
+ }
33
+
34
+ function createRequireBase(base: string) {
35
+ return path.join(base, 'package.json')
36
+ }
37
+
38
+ async function importResolvedModule(resolved: string): Promise<TailwindV4NodeModule> {
39
+ return import(pathToFileURL(resolved).href) as unknown as Promise<TailwindV4NodeModule>
40
+ }
41
+
42
+ async function importTailwindNodeFromBase(base: string): Promise<TailwindV4NodeModule | undefined> {
43
+ try {
44
+ const resolved = createRequire(createRequireBase(base)).resolve('@tailwindcss/node')
45
+ return await importResolvedModule(resolved)
46
+ }
47
+ catch {
48
+ return undefined
49
+ }
50
+ }
51
+
52
+ async function importFallbackTailwindNode(): Promise<TailwindV4NodeModule> {
53
+ return import('@tailwindcss/node') as unknown as Promise<TailwindV4NodeModule>
54
+ }
55
+
56
+ export async function loadTailwindV4NodeModule(baseCandidates: string[]): Promise<TailwindV4NodeModule> {
57
+ const bases = unique(baseCandidates)
58
+ const cacheKey = JSON.stringify(bases)
59
+ const cached = nodeModulePromiseCache.get(cacheKey)
60
+ if (cached) {
61
+ return cached
62
+ }
63
+
64
+ const promise = (async () => {
65
+ for (const base of bases) {
66
+ const loaded = await importTailwindNodeFromBase(base)
67
+ if (loaded) {
68
+ return loaded
69
+ }
70
+ }
71
+
72
+ return importFallbackTailwindNode()
73
+ })()
74
+
75
+ nodeModulePromiseCache.set(cacheKey, promise)
76
+ promise.catch(() => {
77
+ if (nodeModulePromiseCache.get(cacheKey) === promise) {
78
+ nodeModulePromiseCache.delete(cacheKey)
79
+ }
80
+ })
81
+ return promise
82
+ }
83
+
84
+ function createDesignSystemCacheKey(css: string, bases: string[]) {
85
+ return JSON.stringify({
86
+ css,
87
+ bases: unique(bases),
88
+ })
89
+ }
90
+
91
+ export function getTailwindV4DesignSystemCacheKey(source: Pick<TailwindV4ResolvedSource, 'css' | 'base' | 'baseFallbacks'>) {
92
+ return createDesignSystemCacheKey(source.css, [source.base, ...source.baseFallbacks])
93
+ }
94
+
95
+ export async function loadTailwindV4DesignSystem(source: TailwindV4ResolvedSource): Promise<TailwindV4DesignSystem> {
96
+ const bases = unique([source.base, ...source.baseFallbacks])
97
+ if (bases.length === 0) {
98
+ throw new Error('No base directories provided for Tailwind CSS v4 design system.')
99
+ }
100
+
101
+ const cacheKey = createDesignSystemCacheKey(source.css, bases)
102
+ const cached = designSystemPromiseCache.get(cacheKey)
103
+ if (cached) {
104
+ return cached
105
+ }
106
+
107
+ const promise = (async () => {
108
+ const node = await loadTailwindV4NodeModule([source.projectRoot, ...bases])
109
+ let lastError: unknown
110
+
111
+ for (const base of bases) {
112
+ try {
113
+ return await node.__unstable__loadDesignSystem(source.css, { base })
114
+ }
115
+ catch (error) {
116
+ lastError = error
117
+ }
118
+ }
119
+
120
+ if (lastError instanceof Error) {
121
+ throw lastError
122
+ }
123
+ throw new Error('Failed to load Tailwind CSS v4 design system.')
124
+ })()
125
+
126
+ designSystemPromiseCache.set(cacheKey, promise)
127
+ promise.catch(() => {
128
+ if (designSystemPromiseCache.get(cacheKey) === promise) {
129
+ designSystemPromiseCache.delete(cacheKey)
130
+ }
131
+ })
132
+ return promise
133
+ }
134
+
135
+ export async function compileTailwindV4Source(source: TailwindV4ResolvedSource) {
136
+ const node = await loadTailwindV4NodeModule([source.projectRoot, source.base, ...source.baseFallbacks])
137
+ const dependencies = new Set(source.dependencies)
138
+ const compiled = await node.compile(source.css, {
139
+ base: source.base,
140
+ onDependency(dependency) {
141
+ dependencies.add(path.resolve(dependency))
142
+ },
143
+ })
144
+
145
+ return {
146
+ compiled,
147
+ dependencies,
148
+ }
149
+ }
@@ -0,0 +1,193 @@
1
+ import type { NormalizedTailwindCssPatchOptions, TailwindCssPatchOptions } from '../config'
2
+ import type { TailwindV4ResolvedSource, TailwindV4SourceOptions } from './types'
3
+ import { promises as fs } from 'node:fs'
4
+ import process from 'node:process'
5
+ import path from 'pathe'
6
+ import { normalizeOptions } from '../config'
7
+
8
+ function resolveBase(value: string | undefined, fallback: string) {
9
+ return value === undefined
10
+ ? fallback
11
+ : path.isAbsolute(value)
12
+ ? path.resolve(value)
13
+ : path.resolve(fallback, value)
14
+ }
15
+
16
+ function uniquePaths(values: Iterable<string | undefined>) {
17
+ const result: string[] = []
18
+ for (const value of values) {
19
+ if (!value) {
20
+ continue
21
+ }
22
+ const resolved = path.resolve(value)
23
+ if (!result.includes(resolved)) {
24
+ result.push(resolved)
25
+ }
26
+ }
27
+ return result
28
+ }
29
+
30
+ function toCssImportPath(value: string) {
31
+ return value.replaceAll('\\', '/')
32
+ }
33
+
34
+ function quoteCssImport(value: string) {
35
+ return value.replaceAll('\\', '\\\\').replaceAll('"', '\\"')
36
+ }
37
+
38
+ function isPostcssPluginSpecifier(packageName: string) {
39
+ return packageName === '@tailwindcss/postcss'
40
+ || /(?:^|[/\\])@tailwindcss[/\\]postcss(?:[/\\]|$)/.test(packageName)
41
+ || /(?:^|[/\\])postcss(?:[/\\]|$)/i.test(packageName)
42
+ || /postcss\.config\.[cm]?[jt]s$/i.test(packageName)
43
+ }
44
+
45
+ function createDefaultCss(packageName: string | undefined) {
46
+ const cssPackageName = packageName && !isPostcssPluginSpecifier(packageName)
47
+ ? packageName
48
+ : 'tailwindcss'
49
+ return `@import "${quoteCssImport(toCssImportPath(cssPackageName))}";`
50
+ }
51
+
52
+ async function pathExists(filePath: string) {
53
+ try {
54
+ await fs.access(filePath)
55
+ return true
56
+ }
57
+ catch {
58
+ return false
59
+ }
60
+ }
61
+
62
+ async function resolveCssEntries(entries: string[], projectRoot: string, base: string | undefined) {
63
+ const resolvedEntries = entries.map(entry => ({
64
+ original: entry,
65
+ absolute: path.isAbsolute(entry) ? path.resolve(entry) : path.resolve(projectRoot, entry),
66
+ }))
67
+ const resolvedBase = base ?? path.dirname(resolvedEntries[0]?.absolute ?? projectRoot)
68
+ const dependencies = resolvedEntries.map(entry => entry.absolute)
69
+ const cssParts: string[] = []
70
+
71
+ for (const entry of resolvedEntries) {
72
+ if (await pathExists(entry.absolute)) {
73
+ cssParts.push(await fs.readFile(entry.absolute, 'utf8'))
74
+ continue
75
+ }
76
+
77
+ const importPath = path.isAbsolute(entry.original)
78
+ ? entry.absolute
79
+ : path.relative(resolvedBase, entry.absolute)
80
+ cssParts.push(`@import "${quoteCssImport(toCssImportPath(importPath))}";`)
81
+ }
82
+
83
+ return {
84
+ base: resolvedBase,
85
+ css: cssParts.join('\n'),
86
+ dependencies,
87
+ }
88
+ }
89
+
90
+ function normalizeResolvedSource(
91
+ source: {
92
+ projectRoot: string
93
+ cwd: string
94
+ base: string
95
+ baseFallbacks: string[]
96
+ css: string
97
+ dependencies: string[]
98
+ },
99
+ ): TailwindV4ResolvedSource {
100
+ const baseFallbacks = uniquePaths([
101
+ ...source.baseFallbacks,
102
+ source.projectRoot,
103
+ source.cwd,
104
+ ]).filter(base => base !== source.base)
105
+
106
+ return {
107
+ projectRoot: source.projectRoot,
108
+ base: source.base,
109
+ baseFallbacks,
110
+ css: source.css,
111
+ dependencies: Array.from(new Set(source.dependencies.map(dependency => path.resolve(dependency)))),
112
+ }
113
+ }
114
+
115
+ export async function resolveTailwindV4Source(options: TailwindV4SourceOptions = {}): Promise<TailwindV4ResolvedSource> {
116
+ const projectRoot = resolveBase(options.projectRoot, process.cwd())
117
+ const cwd = resolveBase(options.cwd, projectRoot)
118
+ const configuredBase = options.base === undefined ? undefined : resolveBase(options.base, projectRoot)
119
+ const baseFallbacks = uniquePaths(options.baseFallbacks?.map(base => resolveBase(base, projectRoot)) ?? [])
120
+
121
+ if (options.css !== undefined) {
122
+ return normalizeResolvedSource({
123
+ projectRoot,
124
+ cwd,
125
+ base: configuredBase ?? cwd,
126
+ baseFallbacks,
127
+ css: options.css,
128
+ dependencies: [],
129
+ })
130
+ }
131
+
132
+ if (options.cssEntries?.length) {
133
+ const entries = await resolveCssEntries(options.cssEntries, projectRoot, configuredBase)
134
+ return normalizeResolvedSource({
135
+ projectRoot,
136
+ cwd,
137
+ base: entries.base,
138
+ baseFallbacks,
139
+ css: entries.css,
140
+ dependencies: entries.dependencies,
141
+ })
142
+ }
143
+
144
+ return normalizeResolvedSource({
145
+ projectRoot,
146
+ cwd,
147
+ base: configuredBase ?? cwd,
148
+ baseFallbacks,
149
+ css: createDefaultCss(options.packageName),
150
+ dependencies: [],
151
+ })
152
+ }
153
+
154
+ function resolveConfigDir(config: string | undefined, projectRoot: string) {
155
+ if (!config) {
156
+ return undefined
157
+ }
158
+ const configPath = path.isAbsolute(config) ? config : path.resolve(projectRoot, config)
159
+ return path.dirname(configPath)
160
+ }
161
+
162
+ function createSourceOptionsFromNormalizedPatchOptions(
163
+ options: NormalizedTailwindCssPatchOptions,
164
+ ): TailwindV4SourceOptions {
165
+ const v4 = options.tailwind.v4
166
+ const configDir = resolveConfigDir(options.tailwind.config, options.projectRoot)
167
+ const baseFallbacks = uniquePaths([
168
+ v4?.configuredBase,
169
+ options.tailwind.cwd,
170
+ options.projectRoot,
171
+ configDir,
172
+ ])
173
+
174
+ return {
175
+ projectRoot: options.projectRoot,
176
+ ...(options.tailwind.cwd === undefined ? {} : { cwd: options.tailwind.cwd }),
177
+ ...(v4?.configuredBase === undefined ? {} : { base: v4.configuredBase }),
178
+ baseFallbacks,
179
+ ...(v4?.css === undefined ? {} : { css: v4.css }),
180
+ ...(v4?.cssEntries === undefined ? {} : { cssEntries: v4.cssEntries }),
181
+ packageName: options.tailwind.packageName,
182
+ }
183
+ }
184
+
185
+ export function tailwindV4SourceOptionsFromPatchOptions(options: TailwindCssPatchOptions): TailwindV4SourceOptions {
186
+ return createSourceOptionsFromNormalizedPatchOptions(normalizeOptions(options))
187
+ }
188
+
189
+ export async function resolveTailwindV4SourceFromPatchOptions(
190
+ options: TailwindCssPatchOptions,
191
+ ): Promise<TailwindV4ResolvedSource> {
192
+ return resolveTailwindV4Source(tailwindV4SourceOptionsFromPatchOptions(options))
193
+ }