import { parseCandidate } from '../../../../tailwindcss/src/candidate'
import type { DesignSystem } from '../../../../tailwindcss/src/design-system'
import { DefaultMap } from '../../../../tailwindcss/src/utils/default-map'
import * as version from '../../utils/version'
const LOGICAL_OPERATORS = ['&&', '||', '?', '===', '==', '!=', '!==', '>', '>=', '<', '<=']
const CONDITIONAL_TEMPLATE_SYNTAX = [
/(?<!:?class|className)=['"]$/i,
/addEventListener\(['"`]$/,
/wire:[^\s]*?$/,
/variant\s*[:=]\s*\{?['"`]$/,
]
const NEXT_PLACEHOLDER_PROP = /placeholder=\{?['"`]$/
const VUE_3_EMIT = /\b\$?emit\(['"`]$/
export function isSafeMigration(
rawCandidate: string,
location: { contents: string; start: number; end: number },
designSystem: DesignSystem,
): boolean {
if (
location.contents[location.start - 1]?.match(/\s/) &&
location.contents.slice(location.end, location.end + 2)?.match(/^:\s/)
) {
let ranges = styleBlockRanges.get(location.contents)
for (let i = 0; i < ranges.length; i += 2) {
let start = ranges[i]
let end = ranges[i + 1]
if (location.start >= start && location.end <= end) {
return false
}
}
}
let currentLineBeforeCandidate = ''
for (let i = location.start - 1; i >= 0; i--) {
let char = location.contents.at(i)!
if (char === '\n') {
break
}
currentLineBeforeCandidate = char + currentLineBeforeCandidate
}
let currentLineAfterCandidate = ''
for (let i = location.end; i < location.contents.length; i++) {
let char = location.contents.at(i)!
if (char === '\n') {
break
}
currentLineAfterCandidate += char
}
{
let ranges = inlineStyleAttributeValueRanges.get(location.contents)
for (let i = 0; i < ranges.length; i += 2) {
let start = ranges[i]
let end = ranges[i + 1]
if (location.start >= start && location.end <= end) {
return false
}
}
}
let [candidate] = parseCandidate(rawCandidate, designSystem)
if (!candidate && version.isGreaterThan(3)) {
return false
}
else if (candidate) {
if (candidate.variants.length > 0) {
return true
}
if (candidate.kind === 'arbitrary') {
return true
}
if (candidate.kind === 'static' && candidate.root.includes('-')) {
return true
}
if (
(candidate.kind === 'functional' && candidate.value !== null) ||
(candidate.kind === 'functional' && candidate.root.includes('-'))
) {
return true
}
if (candidate.kind === 'functional' && candidate.modifier) {
return true
}
}
let isQuoteBeforeCandidate = isMiddleOfString(currentLineBeforeCandidate)
let isQuoteAfterCandidate = isMiddleOfString(currentLineAfterCandidate)
if (!isQuoteBeforeCandidate || !isQuoteAfterCandidate) {
return false
}
if (currentLineAfterCandidate[0] === '.') {
return false
}
if (currentLineAfterCandidate.trim().startsWith('(')) {
return false
}
for (let operator of LOGICAL_OPERATORS) {
if (
currentLineAfterCandidate.trim().startsWith(operator) ||
currentLineBeforeCandidate.trim().endsWith(operator)
) {
return false
}
}
for (let rule of CONDITIONAL_TEMPLATE_SYNTAX) {
if (rule.test(currentLineBeforeCandidate)) {
return false
}
}
if (NEXT_PLACEHOLDER_PROP.test(currentLineBeforeCandidate)) {
return false
}
if (VUE_3_EMIT.test(currentLineBeforeCandidate)) {
return false
}
return true
}
const styleBlockRanges = new DefaultMap((source: string) => {
let ranges: number[] = []
let offset = 0
while (true) {
let startTag = source.indexOf('<style', offset)
if (startTag === -1) return ranges
offset = startTag + 1
if (!source[startTag + 6].match(/[>\s]/)) continue
let endTag = source.indexOf('</style>', offset)
if (endTag === -1) return ranges
offset = endTag + 1
ranges.push(startTag, endTag)
}
})
const BACKSLASH = 0x5c
const DOUBLE_QUOTE = 0x22
const SINGLE_QUOTE = 0x27
const BACKTICK = 0x60
const TAB = 0x09
const NEWLINE = 0x0a
const FORM_FEED = 0x0c
const CARRIAGE_RETURN = 0x0d
const SPACE = 0x20
const SLASH = 0x2f
const EQUALS = 0x3d
const GREATER_THAN = 0x3e
function isMiddleOfString(line: string): boolean {
let currentQuote: number | null = null
for (let i = 0; i < line.length; i++) {
let char = line.charCodeAt(i)
switch (char) {
case BACKSLASH:
i++
break
case SINGLE_QUOTE:
case DOUBLE_QUOTE:
case BACKTICK:
if (currentQuote === char) {
currentQuote = null
}
else if (currentQuote === null) {
currentQuote = char
}
break
}
}
return currentQuote !== null
}
const inlineStyleAttributeValueRanges = new DefaultMap((source: string) => {
let ranges: number[] = []
let offset = 0
while (true) {
let tagStart = source.indexOf('<', offset)
if (tagStart === -1) return ranges
let tagEnd = source.indexOf('>', tagStart + 1)
if (tagEnd === -1) return ranges
offset = tagEnd + 1
for (let i = tagStart + 1; i < tagEnd; i++) {
let char = source.charCodeAt(i)
if (
char === SPACE ||
char === TAB ||
char === NEWLINE ||
char === CARRIAGE_RETURN ||
char === FORM_FEED
) {
continue
}
let start = i
while (i < tagEnd) {
let char = source.charCodeAt(i)
if (
char === SPACE ||
char === TAB ||
char === NEWLINE ||
char === CARRIAGE_RETURN ||
char === FORM_FEED ||
char === EQUALS ||
char === GREATER_THAN ||
char === SLASH
) {
break
}
i++
}
let attribute = source.slice(start, i).toLowerCase()
if (attribute !== 'style' && attribute !== ':style') continue
while (i < tagEnd) {
let char = source.charCodeAt(i)
if (
char !== SPACE &&
char !== TAB &&
char !== NEWLINE &&
char !== CARRIAGE_RETURN &&
char !== FORM_FEED
) {
break
}
i++
}
if (source[i] !== '=') continue
i++
while (i < tagEnd) {
let char = source.charCodeAt(i)
if (
char !== SPACE &&
char !== TAB &&
char !== NEWLINE &&
char !== CARRIAGE_RETURN &&
char !== FORM_FEED
) {
break
}
i++
}
let quote = source[i]
if (quote !== '"' && quote !== "'") continue
let valueStart = i + 1
let valueEnd = source.indexOf(quote, valueStart)
if (valueEnd === -1 || valueEnd > tagEnd) break
ranges.push(valueStart, valueEnd)
i = valueEnd
}
}
})