Skip to content

Commit d61a31a

Browse files
committed
Add first implementation
1 parent 0919944 commit d61a31a

File tree

2 files changed

+167
-4
lines changed

2 files changed

+167
-4
lines changed

index.js

Lines changed: 91 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,96 @@
1+
function parseParameters(paramString) {
2+
let params = []
3+
let current = ''
4+
let depth = 0
5+
let inQuotes = false
6+
let quoteChar = ''
7+
8+
for (let i = 0; i < paramString.length; i++) {
9+
let char = paramString[i]
10+
11+
if (!inQuotes && (char === '"' || char === "'")) {
12+
inQuotes = true
13+
quoteChar = char
14+
current += char
15+
} else if (inQuotes && char === quoteChar) {
16+
inQuotes = false
17+
quoteChar = ''
18+
current += char
19+
} else if (!inQuotes && char === '(') {
20+
depth++
21+
current += char
22+
} else if (!inQuotes && char === ')') {
23+
depth--
24+
current += char
25+
} else if (!inQuotes && char === ',' && depth === 0) {
26+
params.push(current.trim())
27+
current = ''
28+
} else {
29+
current += char
30+
}
31+
}
32+
33+
if (current.trim()) {
34+
params.push(current.trim())
35+
}
36+
37+
return params
38+
}
39+
40+
function findMatchingParen(str, startIndex) {
41+
let depth = 1
42+
for (let i = startIndex + 1; i < str.length; i++) {
43+
if (str[i] === '(') depth++
44+
else if (str[i] === ')') depth--
45+
if (depth === 0) return i
46+
}
47+
return -1
48+
}
49+
150
module.exports = () => {
251
return {
3-
Declaration() {},
52+
Declaration(decl) {
53+
if (decl.value.includes('--smooth-shadow(')) {
54+
let result = decl.value
55+
let index = 0
56+
57+
let funcStart = result.indexOf('--smooth-shadow(', index)
58+
for (
59+
;
60+
funcStart !== -1;
61+
funcStart = result.indexOf('--smooth-shadow(', index)
62+
) {
63+
let parenStart = funcStart + '--smooth-shadow('.length - 1
64+
let parenEnd = findMatchingParen(result, parenStart)
65+
66+
if (parenEnd === -1) break
67+
68+
let paramString = result.substring(parenStart + 1, parenEnd)
69+
let params = parseParameters(paramString)
70+
71+
if (params.length < 2 || params.length > 3) {
72+
throw decl.error(
73+
'--smooth-shadow() requires 2 or 3 parameters: color, size, and optional spread (defaults to 3)'
74+
)
75+
}
76+
77+
let [color, size, spread = '3'] = params
78+
79+
let replacement =
80+
`0 calc(${size} / ${spread}) calc(${size} / ${spread} * 2) rgb(from ${color} r g b / 0.25), ` +
81+
`0 calc(${size} / ${spread} * 2) calc(${size} / ${spread} * 3) rgb(from ${color} r g b / 0.18), ` +
82+
`0 calc(${size} / ${spread} * 3) calc(${size} / ${spread} * 4) rgb(from ${color} r g b / 0.12)`
83+
84+
result =
85+
result.substring(0, funcStart) +
86+
replacement +
87+
result.substring(parenEnd + 1)
88+
index = funcStart + replacement.length
89+
}
90+
91+
decl.value = result
92+
}
93+
},
494
postcssPlugin: 'postcss-smooth-shadow'
595
}
696
}

index.test.js

Lines changed: 76 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
let { equal } = require('node:assert')
1+
let { equal, throws } = require('node:assert/strict')
22
let { test } = require('node:test')
33
let postcss = require('postcss')
44

@@ -11,6 +11,79 @@ function run(input, output, opts) {
1111
return result
1212
}
1313

14-
test('exists', () => {
15-
run('', '')
14+
test('replaces --smooth-shadow with layered box shadows', () => {
15+
run(
16+
'.card { box-shadow: --smooth-shadow(#000, 10px, 2); }',
17+
'.card { box-shadow: 0 calc(10px / 2) calc(10px / 2 * 2) rgb(from #000 r g b / 0.25), 0 calc(10px / 2 * 2) calc(10px / 2 * 3) rgb(from #000 r g b / 0.18), 0 calc(10px / 2 * 3) calc(10px / 2 * 4) rgb(from #000 r g b / 0.12); }'
18+
)
19+
})
20+
21+
test('handles CSS variables as parameters', () => {
22+
run(
23+
'.element { box-shadow: --smooth-shadow(var(--shadow-color), var(--shadow-size), var(--shadow-spread)); }',
24+
'.element { box-shadow: 0 calc(var(--shadow-size) / var(--shadow-spread)) calc(var(--shadow-size) / var(--shadow-spread) * 2) rgb(from var(--shadow-color) r g b / 0.25), 0 calc(var(--shadow-size) / var(--shadow-spread) * 2) calc(var(--shadow-size) / var(--shadow-spread) * 3) rgb(from var(--shadow-color) r g b / 0.18), 0 calc(var(--shadow-size) / var(--shadow-spread) * 3) calc(var(--shadow-size) / var(--shadow-spread) * 4) rgb(from var(--shadow-color) r g b / 0.12); }'
25+
)
26+
})
27+
28+
test('uses default spread value of 3 when omitted', () => {
29+
run(
30+
'.card { box-shadow: --smooth-shadow(#000, 12px); }',
31+
'.card { box-shadow: 0 calc(12px / 3) calc(12px / 3 * 2) rgb(from #000 r g b / 0.25), 0 calc(12px / 3 * 2) calc(12px / 3 * 3) rgb(from #000 r g b / 0.18), 0 calc(12px / 3 * 3) calc(12px / 3 * 4) rgb(from #000 r g b / 0.12); }'
32+
)
33+
})
34+
35+
test('handles mix of two and three parameter calls', () => {
36+
run(
37+
'.mixed { box-shadow: --smooth-shadow(#000, 6px), --smooth-shadow(#fff, 4px, 2); }',
38+
'.mixed { box-shadow: 0 calc(6px / 3) calc(6px / 3 * 2) rgb(from #000 r g b / 0.25), 0 calc(6px / 3 * 2) calc(6px / 3 * 3) rgb(from #000 r g b / 0.18), 0 calc(6px / 3 * 3) calc(6px / 3 * 4) rgb(from #000 r g b / 0.12), 0 calc(4px / 2) calc(4px / 2 * 2) rgb(from #fff r g b / 0.25), 0 calc(4px / 2 * 2) calc(4px / 2 * 3) rgb(from #fff r g b / 0.18), 0 calc(4px / 2 * 3) calc(4px / 2 * 4) rgb(from #fff r g b / 0.12); }'
39+
)
40+
})
41+
42+
test('handles multiple --smooth-shadow calls in same declaration', () => {
43+
run(
44+
'.card { box-shadow: --smooth-shadow(#000, 5px, 1), inset --smooth-shadow(#fff, 2px, 1); }',
45+
'.card { box-shadow: 0 calc(5px / 1) calc(5px / 1 * 2) rgb(from #000 r g b / 0.25), 0 calc(5px / 1 * 2) calc(5px / 1 * 3) rgb(from #000 r g b / 0.18), 0 calc(5px / 1 * 3) calc(5px / 1 * 4) rgb(from #000 r g b / 0.12), inset 0 calc(2px / 1) calc(2px / 1 * 2) rgb(from #fff r g b / 0.25), 0 calc(2px / 1 * 2) calc(2px / 1 * 3) rgb(from #fff r g b / 0.18), 0 calc(2px / 1 * 3) calc(2px / 1 * 4) rgb(from #fff r g b / 0.12); }'
46+
)
47+
})
48+
49+
test('works with different color formats', () => {
50+
run(
51+
'.test { box-shadow: --smooth-shadow(rgb(255, 0, 0), 8px, 2); }',
52+
'.test { box-shadow: 0 calc(8px / 2) calc(8px / 2 * 2) rgb(from rgb(255, 0, 0) r g b / 0.25), 0 calc(8px / 2 * 2) calc(8px / 2 * 3) rgb(from rgb(255, 0, 0) r g b / 0.18), 0 calc(8px / 2 * 3) calc(8px / 2 * 4) rgb(from rgb(255, 0, 0) r g b / 0.12); }'
53+
)
54+
})
55+
56+
test('handles decimal values', () => {
57+
run(
58+
'.button { box-shadow: --smooth-shadow(#333, 4.5px, 1.5); }',
59+
'.button { box-shadow: 0 calc(4.5px / 1.5) calc(4.5px / 1.5 * 2) rgb(from #333 r g b / 0.25), 0 calc(4.5px / 1.5 * 2) calc(4.5px / 1.5 * 3) rgb(from #333 r g b / 0.18), 0 calc(4.5px / 1.5 * 3) calc(4.5px / 1.5 * 4) rgb(from #333 r g b / 0.12); }'
60+
)
61+
})
62+
63+
test('leaves other declarations unchanged', () => {
64+
run(
65+
'.normal { color: red; background: blue; }',
66+
'.normal { color: red; background: blue; }'
67+
)
68+
})
69+
70+
test('works in other properties besides box-shadow', () => {
71+
run(
72+
'.custom { filter: drop-shadow(--smooth-shadow(#000, 3px, 1)); }',
73+
'.custom { filter: drop-shadow(0 calc(3px / 1) calc(3px / 1 * 2) rgb(from #000 r g b / 0.25), 0 calc(3px / 1 * 2) calc(3px / 1 * 3) rgb(from #000 r g b / 0.18), 0 calc(3px / 1 * 3) calc(3px / 1 * 4) rgb(from #000 r g b / 0.12)); }'
74+
)
75+
})
76+
77+
test('throws error for insufficient parameters', () => {
78+
let input = '.error { box-shadow: --smooth-shadow(#000); }'
79+
throws(() => {
80+
postcss([plugin]).process(input, { from: undefined }).css
81+
}, '--smooth-shadow() requires 2 or 3 parameters')
82+
})
83+
84+
test('throws error for too many parameters', () => {
85+
let input = '.error { box-shadow: --smooth-shadow(#000, 10px, 2, extra); }'
86+
throws(() => {
87+
postcss([plugin]).process(input, { from: undefined }).css
88+
}, '--smooth-shadow() requires 2 or 3 parameters')
1689
})

0 commit comments

Comments
 (0)