Skip to content

Commit

Permalink
New keyword: single-arg (#162)
Browse files Browse the repository at this point in the history
* - Introduced `asSingleArg` keyword to handle passing comma-separated CSS values as mixins' arguments
- Included prettier version in the dev dependencies for the formatting consistency

* README

* PR fixes part1

* PR fixes part2

* asSingleArg => single-arg

* grammar
  • Loading branch information
pciarach committed Mar 4, 2024
1 parent 6630823 commit 5a288dd
Show file tree
Hide file tree
Showing 3 changed files with 159 additions and 12 deletions.
15 changes: 15 additions & 0 deletions README.md
Expand Up @@ -257,6 +257,21 @@ could be used like this:
.isIE .foo { color: red; }
```

### Mixin parameters with comma

In order to pass a comma-separated value as an argument to a mixin, you can use
the special `single-arg` keyword. For example:

```css
@define-mixin transition $properties, $duration {
transition-property: $properties;
transition-duration: $duration;
}

.foo {
@mixin transition single-arg(color, background-color), 0.5s;
}
```

### Migration from Sass

Expand Down
46 changes: 43 additions & 3 deletions index.js
Expand Up @@ -111,15 +111,48 @@ function processMixinContent(rule, from) {
})
}

function insertObject(rule, obj) {
function insertObject(rule, obj, singeArgumentsMap) {
let root = parse(obj)
root.each(node => {
node.source = rule.source
})
processMixinContent(root, rule)
unwrapSingleArguments(root.nodes, singeArgumentsMap)
rule.parent.insertBefore(rule, root)
}

function unwrapSingleArguments(rules, singleArgumentsMap) {
if (singleArgumentsMap.size <= 0) {
return
}

for (let rule of rules) {
if (rule.type === 'decl') {
if (rule.value.includes('single-arg')) {
let newValue = rule.value
for (let [key, value] of singleArgumentsMap) {
newValue = newValue.replace(key, value)
}
rule.value = newValue
}
} else if (rule.type === 'rule') {
unwrapSingleArguments(rule.nodes, singleArgumentsMap)
}
}
}

function resolveSingleArgumentValue(value, parentNode) {
let content = value.slice('single-arg'.length).trim()

if (!content.startsWith('(') || !content.endsWith(')')) {
throw parentNode.error(
'Content of single-arg must be wrapped in brackets: ' + value
)
}

return content.slice(1, -1)
}

function insertMixin(helpers, mixins, rule, opts) {
let name = rule.params.split(/\s/, 1)[0]
let rest = rule.params.slice(name.length).trim()
Expand All @@ -139,6 +172,11 @@ function insertMixin(helpers, mixins, rule, opts) {

let meta = mixins[name]
let mixin = meta && meta.mixin
let singleArgumentsMap = new Map(
params
.filter(param => param.startsWith('single-arg'))
.map(param => [param, resolveSingleArgumentValue(param, rule)])
)

if (!meta) {
if (!opts.silent) {
Expand All @@ -164,9 +202,11 @@ function insertMixin(helpers, mixins, rule, opts) {

if (meta.content) processMixinContent(proxy, rule)

unwrapSingleArguments(proxy.nodes, singleArgumentsMap)

rule.parent.insertBefore(rule, proxy)
} else if (typeof mixin === 'object') {
insertObject(rule, mixin)
insertObject(rule, mixin, singleArgumentsMap)
} else if (typeof mixin === 'function') {
let args = [rule].concat(params)
rule.walkAtRules(atRule => {
Expand All @@ -176,7 +216,7 @@ function insertMixin(helpers, mixins, rule, opts) {
})
let nodes = mixin(...args)
if (typeof nodes === 'object') {
insertObject(rule, nodes)
insertObject(rule, nodes, singleArgumentsMap)
}
} else {
throw new Error('Wrong ' + name + ' mixin type ' + typeof mixin)
Expand Down
110 changes: 101 additions & 9 deletions test/index.test.js
Expand Up @@ -42,9 +42,12 @@ test('does not throw error on brackets in at-rules inside function mixins', asyn
'.a { @supports (max(0px)) { color: black; } }',
{
mixins: {
a() { return { '.a': { '@mixin-content' : {} } } }
a() {
return { '.a': { '@mixin-content': {} } }
}
}
})
}
)
})

test('cans remove unknown mixin on request', async () => {
Expand Down Expand Up @@ -342,13 +345,9 @@ test('loads mixins from file globs', async () => {
})

test('loads mixins with dependencies', async () => {
let result = await run(
'a { @mixin f; }',
'a { g: 5; }',
{
mixinsFiles: join(__dirname, 'deps', 'f.js')
}
)
let result = await run('a { @mixin f; }', 'a { g: 5; }', {
mixinsFiles: join(__dirname, 'deps', 'f.js')
})
equal(
result.messages.sort((a, b) => a.file && a.file.localeCompare(b.file)),
[
Expand Down Expand Up @@ -427,4 +426,97 @@ test('has @add-mixin alias', async () => {
await run('@define-mixin a { a: 1 } @add-mixin a', 'a: 1')
})

test('treats single-arg content as a single argument', async () => {
await run(
'@define-mixin a $x, $y { a: $x; b: $y; } ' +
'@mixin a single-arg(1, 2), 3;',
'a: 1, 2;\nb: 3;'
)
})

test('throws error when single-arg does not have start parenthesis', async () => {
let error = await catchError(() =>
run('@define-mixin a $p {}; @mixin a single-arg 1, 2);')
)

equal(
error.message,
'postcss-mixins: <css input>:1:24: ' +
'Content of single-arg must be wrapped in brackets: single-arg 1'
)
})

test('throws error when single-arg does not have end parenthesis', async () => {
let error = await catchError(() =>
run('@define-mixin a $p {}; @mixin a single-arg(1, 2;')
)

equal(
error.message,
'postcss-mixins: <css input>:1:24: ' +
'Content of single-arg must be wrapped in brackets: single-arg(1, 2;'
)
})

test('ignores whitespaces outside of single-arg parentheses', async () => {
await run(
'@define-mixin a $x, $y { a: $x; b: $y; } ' +
'@mixin a single-arg (1, 2) , 3;',
'a: 1, 2;\nb: 3;'
)
})

test('can replace multiple single-arg contents', async () => {
await run(
'@define-mixin a $x, $y { a: $x; b: $y; } ' +
'@mixin a single-arg(1, 2), single-arg(3, 4);',
'a: 1, 2;\nb: 3, 4;'
)
})

test('can replace multiple single-arg contents inside single declaration', async () => {
await run(
'@define-mixin a $x, $y { a: $x, $y; } ' +
'@mixin a single-arg(1, 2), single-arg(3, 4);',
'a: 1, 2, 3, 4;'
)
})

test('can replace single-arg contents with nested parentheses', async () => {
await run(
'@define-mixin a $x { a: $x } ' + '@mixin a single-arg(1, (2), 3);',
'a: 1, (2), 3;'
)
})

test('handles single-arg inside rules', async () => {
await run(
'@define-mixin a $x, $y { .s { a: $x; b: $y; } } ' +
'@mixin a single-arg(1, 2), 3;',
'.s { a: 1, 2; b: 3; }'
)
})

test('passes single-arg to the nested mixin', async () => {
await run(
'@define-mixin a $p { a: $p; } ' +
'@define-mixin b $x, $y { @mixin a $x; b: $y; } ' +
'@mixin b single-arg(1, 2), 3;',
'a: 1, 2;\nb: 3;'
)
})

test('passes single-arg to the nested function mixin', async () => {
await run('@mixin b single-arg(1, 2), 3;', 'a: 1, 2;\nb: 3;', {
mixins: {
a(rule, p) {
return { a: p }
},
b(rule, x, y) {
return { ['@mixin a ' + x]: {}, b: y }
}
}
})
})

test.run()

0 comments on commit 5a288dd

Please sign in to comment.