Skip to content

Commit 4ae946f

Browse files
committed
css output nits
1 parent a6f7a45 commit 4ae946f

1 file changed

Lines changed: 98 additions & 50 deletions

File tree

src/utils/gradientString.ts

Lines changed: 98 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -71,68 +71,116 @@ function stopsToStrings(stops: any[], { convert_colors, new_lines }: { convert_c
7171
return str + '%'
7272
}
7373

74+
function asNumberPercent(p: any): number | null {
75+
if (p == null) return null
76+
// Accept numbers, unitless numeric strings, or percent strings
77+
if (typeof p === 'number' && !Number.isNaN(p)) return p
78+
const str = String(p).trim()
79+
if (/^[-+]?\d*\.?\d+%$/.test(str)) return Number(str.replace('%',''))
80+
if (/^[-+]?\d*\.?\d+$/.test(str)) return Number(str)
81+
return null
82+
}
83+
7484
function isPctZero(p: any) {
75-
if (p == null) return false
76-
const m = String(p).match(/^(-?\d+(?:\.\d+)?)%$/)
77-
return !!(m && Number(m[1]) === 0)
85+
const n = asNumberPercent(p)
86+
return n != null && Number(n) === 0
7887
}
7988

8089
function isPctHundred(p: any) {
81-
if (p == null) return false
82-
const m = String(p).match(/^(-?\d+(?:\.\d+)?)%$/)
83-
return !!(m && Number(m[1]) === 100)
90+
const n = asNumberPercent(p)
91+
return n != null && Number(n) === 100
8492
}
8593

8694
function isPctFifty(p: any) {
87-
if (p == null) return false
88-
const m = String(p).match(/^(-?\d+(?:\.\d+)?)%$/)
89-
return !!(m && Number(m[1]) === 50)
95+
const n = asNumberPercent(p)
96+
return n != null && Number(n) === 50
9097
}
9198

92-
return stops
93-
.map((s, i) => {
94-
if (s.kind === 'stop') {
95-
let p1 = s.position1
96-
let p2 = s.position2
99+
type StopOut = { kind: 'stop'; color: string; posA?: string | null; posB?: string | null } | { kind: 'hint'; text: string }
100+
const out: StopOut[] = []
97101

98-
// If first position equals computed auto position, omit it (keep explicit second positions)
99-
if (p1 != null && s.auto != null && String(p1) == String(s.auto)) p1 = null
102+
for (let i = 0; i < stops.length; i++) {
103+
const s = stops[i]
104+
if (!s) continue
105+
if (s.kind === 'stop') {
106+
let p1: any = s.position1
107+
let p2: any = s.position2
100108

101-
// Omit default endpoints
102-
if (i === firstStopIdx && isPctZero(p1)) p1 = null
103-
if (i === lastStopIdx && isPctHundred(p2)) p2 = null
109+
// If positions equal computed auto position, omit them
110+
if (p1 != null && s.auto != null && String(p1) == String(s.auto)) p1 = null
111+
if (p2 != null && s.auto != null && String(p2) == String(s.auto)) p2 = null
104112

105-
if (p1 != null && p2 != null) {
106-
const a = fmtPos(p1)
107-
const b = fmtPos(p2)
108-
if (a !== b) return maybeConvertColor(s.color, convert_colors) + ' ' + a + ' ' + b
109-
return maybeConvertColor(s.color, convert_colors) + ' ' + a
110-
}
113+
// Omit default endpoints regardless of whether value is in p1 or p2
114+
if (i === firstStopIdx) {
115+
if (isPctZero(p1)) p1 = null
116+
if (isPctZero(p2) && p1 == null) p2 = null
117+
}
118+
if (i === lastStopIdx) {
119+
if (isPctHundred(p2)) p2 = null
120+
if (isPctHundred(p1) && p2 == null) p1 = null
121+
}
111122

112-
if (p1 == null && p2 != null) {
113-
const b = fmtPos(p2)
114-
return maybeConvertColor(s.color, convert_colors) + ' ' + b
115-
}
123+
const colorStr = maybeConvertColor(s.color, convert_colors)
116124

117-
if (p1 != null && p2 == null) {
118-
const a = fmtPos(p1)
119-
return maybeConvertColor(s.color, convert_colors) + ' ' + a
125+
// Normalize: if both positions present and equal, reduce to one
126+
if (p1 != null && p2 != null) {
127+
const a = fmtPos(p1)
128+
const b = fmtPos(p2)
129+
if (a === b) {
130+
out.push({ kind: 'stop', color: colorStr, posA: a })
131+
} else {
132+
out.push({ kind: 'stop', color: colorStr, posA: a, posB: b })
120133
}
134+
continue
135+
}
121136

122-
return maybeConvertColor(s.color, convert_colors)
137+
if (p1 == null && p2 != null) {
138+
out.push({ kind: 'stop', color: colorStr, posA: fmtPos(p2) })
139+
continue
123140
}
124-
else if (s.kind === 'hint') {
125-
// Omit default/auto hints (like 50%)
126-
const pct = s.percentage
127-
if (pct == null) return null
128-
if (s.auto != null && String(pct) == String(s.auto)) return null
129-
if (isPctFifty(pct)) return null
130-
return pct + '%'
141+
142+
if (p1 != null && p2 == null) {
143+
out.push({ kind: 'stop', color: colorStr, posA: fmtPos(p1) })
144+
continue
131145
}
132-
return null
133-
})
134-
.filter(Boolean)
135-
.join(new_lines === true ? ',\n ' : ', ')
146+
147+
out.push({ kind: 'stop', color: colorStr })
148+
}
149+
else if (s.kind === 'hint') {
150+
// Omit default/auto hints (like 50%)
151+
const pct = s.percentage
152+
if (pct == null) continue
153+
if (s.auto != null && String(pct) == String(s.auto)) continue
154+
if (isPctFifty(pct)) continue
155+
out.push({ kind: 'hint', text: pct + '%' })
156+
}
157+
}
158+
159+
// Decide whether to use multi-line formatting based on color token lengths
160+
const colorStops = out.filter((x): x is Extract<StopOut, {kind: 'stop'}> => x.kind === 'stop')
161+
const maxColorLen = colorStops.reduce((m, s) => Math.max(m, s.color.length), 0)
162+
const hasLongColor = maxColorLen >= 20 || colorStops.some(s => /\(|\s/.test(s.color))
163+
const useNewLines = new_lines === true || (new_lines !== false && hasLongColor)
164+
165+
if (!useNewLines) {
166+
return out.map(s => {
167+
if (s.kind === 'hint') return s.text
168+
const parts = [s.color]
169+
if (s.posA) parts.push(s.posA)
170+
if (s.posB) parts.push(s.posB)
171+
return parts.join(' ')
172+
}).join(', ')
173+
}
174+
175+
// Multi-line with aligned positions
176+
return out.map(s => {
177+
if (s.kind === 'hint') return s.text
178+
const pad = s.posA || s.posB ? ' '.repeat(Math.max(1, maxColorLen - s.color.length + 1)) : ''
179+
const parts = [s.color]
180+
if (s.posA) parts.push(pad + s.posA)
181+
if (s.posB) parts.push(' ' + s.posB) // second position separated by single space
182+
return parts.join('')
183+
}).join(',\n ')
136184
}
137185

138186
function linearAngleToken(linear: LayerSnapshot['linear']) {
@@ -154,39 +202,39 @@ function linearAngleToken(linear: LayerSnapshot['linear']) {
154202
function modernString(layer: LayerSnapshot) {
155203
if (layer.type === 'linear') {
156204
const tokens = [linearAngleToken(layer.linear), spaceToString(layer.space, layer.interpolation)].filter(Boolean).join(' ')
157-
return `linear-gradient(\n ${tokens},\n ${stopsToStrings(layer.stops, { new_lines: false })}\n )`
205+
return `linear-gradient(\n ${tokens},\n ${stopsToStrings(layer.stops)}\n )`
158206
}
159207
else if (layer.type === 'radial') {
160208
const pos = radialPositionToString(layer.radial)
161209
const posPart = pos && pos !== 'center' ? 'at ' + pos : ''
162210
const tokens = [layer.radial.size, layer.radial.shape, posPart, spaceToString(layer.space, layer.interpolation)].filter(Boolean).join(' ')
163-
return `radial-gradient(\n ${tokens},\n ${stopsToStrings(layer.stops, { new_lines: false })}\n )`
211+
return `radial-gradient(\n ${tokens},\n ${stopsToStrings(layer.stops)}\n )`
164212
}
165213
else {
166214
const pos = conicPositionToString(layer.conic)
167215
const posPart = pos && pos !== 'center' ? 'at ' + pos : ''
168216
const fromPart = (Number(layer.conic.angle) || 0) % 360 === 0 ? '' : `from ${layer.conic.angle}deg`
169217
const tokens = [fromPart, posPart, spaceToString(layer.space, layer.interpolation)].filter(Boolean).join(' ')
170-
return `conic-gradient(\n ${tokens},\n ${stopsToStrings(layer.stops, { new_lines: false })}\n )`
218+
return `conic-gradient(\n ${tokens},\n ${stopsToStrings(layer.stops)}\n )`
171219
}
172220
}
173221

174222
function classicString(layer: LayerSnapshot) {
175223
if (layer.type === 'linear') {
176224
const angleToken = linearAngleToken(layer.linear)
177225
const header = angleToken ? angleToken + ', ' : ''
178-
return `linear-gradient(${header}${stopsToStrings(layer.stops, { convert_colors: true, new_lines: false })})`
226+
return `linear-gradient(${header}${stopsToStrings(layer.stops, { convert_colors: true })})`
179227
}
180228
else if (layer.type === 'radial') {
181229
const pos = radialPositionToString(layer.radial)
182230
const posPart = pos && pos !== 'center' ? ' at ' + pos : ''
183-
return `radial-gradient(${layer.radial.size} ${layer.radial.shape}${posPart}, ${stopsToStrings(layer.stops, { convert_colors: true, new_lines: false })})`
231+
return `radial-gradient(${layer.radial.size} ${layer.radial.shape}${posPart}, ${stopsToStrings(layer.stops, { convert_colors: true })})`
184232
}
185233
else {
186234
const pos = conicPositionToString(layer.conic)
187235
const posPart = pos && pos !== 'center' ? ' at ' + pos : ''
188236
const fromPart = (Number(layer.conic.angle) || 0) % 360 === 0 ? '' : `from ${layer.conic.angle}deg `
189-
return `conic-gradient(${fromPart.trim()}${posPart}, ${stopsToStrings(layer.stops, { convert_colors: true, new_lines: false })})`
237+
return `conic-gradient(${fromPart.trim()}${posPart}, ${stopsToStrings(layer.stops, { convert_colors: true })})`
190238
}
191239
}
192240

0 commit comments

Comments
 (0)