Skip to content

Commit

Permalink
Add support for the @content rule (#2501)
Browse files Browse the repository at this point in the history
Co-authored-by: Carlos (Goodwine) <2022649+Goodwine@users.noreply.github.com>
  • Loading branch information
nex3 and Goodwine authored Jan 30, 2025
1 parent 3b46880 commit aed7839
Show file tree
Hide file tree
Showing 10 changed files with 495 additions and 8 deletions.
5 changes: 3 additions & 2 deletions lib/src/js/parser.dart
Original file line number Diff line number Diff line change
Expand Up @@ -93,9 +93,10 @@ void _updateAstPrototypes() {
(Expression self, ExpressionVisitor<Object?> visitor) =>
self.accept(visitor));
var arguments = ArgumentList([], {}, bogusSpan);
var include = IncludeRule('a', arguments, bogusSpan);
getJSClass(include)
getJSClass(IncludeRule('a', arguments, bogusSpan))
.defineGetter('arguments', (IncludeRule self) => self.arguments);
getJSClass(ContentRule(arguments, bogusSpan))
.defineGetter('arguments', (ContentRule self) => self.arguments);

_addSupportsConditionToInterpolation();

Expand Down
5 changes: 5 additions & 0 deletions pkg/sass-parser/lib/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,11 @@ export {
ParameterProps,
Parameter,
} from './src/parameter';
export {
ContentRule,
ContentRuleProps,
ContentRuleRaws,
} from './src/statement/content-rule';
export {
CssComment,
CssCommentProps,
Expand Down
6 changes: 6 additions & 0 deletions pkg/sass-parser/lib/src/sass-internal.ts
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,10 @@ declare namespace SassInternal {
readonly parameters: ParameterList;
}

class ContentRule extends Statement {
readonly arguments: ArgumentList;
}

class DebugRule extends Statement {
readonly expression: Expression;
}
Expand Down Expand Up @@ -348,6 +352,7 @@ export type ArgumentList = SassInternal.ArgumentList;
export type AtRootRule = SassInternal.AtRootRule;
export type AtRule = SassInternal.AtRule;
export type ContentBlock = SassInternal.ContentBlock;
export type ContentRule = SassInternal.ContentRule;
export type DebugRule = SassInternal.DebugRule;
export type Declaration = SassInternal.Declaration;
export type EachRule = SassInternal.EachRule;
Expand Down Expand Up @@ -388,6 +393,7 @@ export type NumberExpression = SassInternal.NumberExpression;
export interface StatementVisitorObject<T> {
visitAtRootRule(node: AtRootRule): T;
visitAtRule(node: AtRule): T;
visitContentRule(node: ContentRule): T;
visitDebugRule(node: DebugRule): T;
visitDeclaration(node: Declaration): T;
visitEachRule(node: EachRule): T;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`a @content rule toJSON 1`] = `
{
"contentArguments": <(bar)>,
"inputs": [
{
"css": "@mixin foo {@content(bar)}",
"hasBOM": false,
"id": "<input css _____>",
},
],
"name": "content",
"params": "(bar)",
"raws": {},
"sassType": "content-rule",
"source": <1:13-1:26 in 0>,
"type": "atrule",
}
`;
306 changes: 306 additions & 0 deletions pkg/sass-parser/lib/src/statement/content-rule.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,306 @@
// Copyright 2025 Google Inc. Use of this source code is governed by an
// MIT-style license that can be found in the LICENSE file or at
// https://opensource.org/licenses/MIT.

import {ArgumentList, ContentRule, MixinRule, sass, scss} from '../..';
import * as utils from '../../../test/utils';

describe('a @content rule', () => {
let node: ContentRule;
describe('without arguments', () => {
function describeNode(
description: string,
create: () => ContentRule,
): void {
describe(description, () => {
beforeEach(() => void (node = create()));

it('has a sassType', () => expect(node.sassType).toBe('content-rule'));

it('has a name', () => expect(node.name.toString()).toBe('content'));

it('has no arguments', () =>
expect(node.contentArguments.nodes).toHaveLength(0));

it('has matching params', () => expect(node.params).toBe(''));

it('has undefined nodes', () => expect(node.nodes).toBeUndefined());
});
}

describe('parsed as SCSS', () => {
describeNode(
'without parens',
() =>
(scss.parse('@mixin foo {@content}').nodes[0] as MixinRule)
.nodes[0] as ContentRule,
);

describeNode(
'with parens',
() =>
(scss.parse('@mixin foo {@content()}').nodes[0] as MixinRule)
.nodes[0] as ContentRule,
);
});

describe('parsed as Sass', () => {
describeNode(
'without parens',
() =>
(sass.parse('@mixin foo\n @content').nodes[0] as MixinRule)
.nodes[0] as ContentRule,
);

describeNode(
'with parens',
() =>
(sass.parse('@mixin foo\n @content()').nodes[0] as MixinRule)
.nodes[0] as ContentRule,
);
});

describe('constructed manually', () => {
describeNode(
'with defined contentArguments',
() => new ContentRule({contentArguments: []}),
);

describeNode(
'with undefined contentArguments',
() => new ContentRule({contentArguments: undefined}),
);

describeNode('without contentArguments', () => new ContentRule());
});

describe('constructed from ChildProps', () => {
describeNode('with defined contentArguments', () =>
utils.fromChildProps({contentArguments: []}),
);

describeNode('with undefined contentArguments', () =>
utils.fromChildProps({contentArguments: undefined}),
);
});
});

describe('with arguments', () => {
function describeNode(
description: string,
create: () => ContentRule,
): void {
describe(description, () => {
beforeEach(() => void (node = create()));

it('has a sassType', () => expect(node.sassType).toBe('content-rule'));

it('has a name', () => expect(node.name.toString()).toBe('content'));

it('has an argument', () =>
expect(node.contentArguments.nodes[0]).toHaveStringExpression(
'value',
'bar',
));

it('has matching params', () => expect(node.params).toBe('(bar)'));

it('has undefined nodes', () => expect(node.nodes).toBeUndefined());
});
}

describeNode(
'parsed as SCSS',
() =>
(scss.parse('@mixin foo {@content(bar)}').nodes[0] as MixinRule)
.nodes[0] as ContentRule,
);

describeNode(
'parsed as Sass',
() =>
(sass.parse('@mixin foo\n @content(bar)').nodes[0] as MixinRule)
.nodes[0] as ContentRule,
);

describeNode(
'constructed manually',
() => new ContentRule({contentArguments: [{text: 'bar'}]}),
);

describeNode('constructed from ChildProps', () =>
utils.fromChildProps({contentArguments: [{text: 'bar'}]}),
);
});

describe('throws an error when assigned a new', () => {
beforeEach(
() => void (node = new ContentRule({contentArguments: [{text: 'bar'}]})),
);

it('name', () => expect(() => (node.name = 'qux')).toThrow());

it('params', () => expect(() => (node.params = '(zap)')).toThrow());
});

describe('assigned new arguments', () => {
beforeEach(
() => void (node = new ContentRule({contentArguments: [{text: 'bar'}]})),
);

it("removes the old arguments' parent", () => {
const oldArguments = node.contentArguments;
node.contentArguments = [{text: 'qux'}];
expect(oldArguments.parent).toBeUndefined();
});

it("assigns the new arguments' parent", () => {
const args = new ArgumentList([{text: 'qux'}]);
node.contentArguments = args;
expect(args.parent).toBe(node);
});

it('assigns the arguments explicitly', () => {
const args = new ArgumentList([{text: 'qux'}]);
node.contentArguments = args;
expect(node.contentArguments).toBe(args);
});

it('assigns the expression as ArgumentProps', () => {
node.contentArguments = [{text: 'qux'}];
expect(node.contentArguments.nodes[0]).toHaveStringExpression(
'value',
'qux',
);
expect(node.contentArguments.parent).toBe(node);
});
});

describe('stringifies', () => {
describe('to SCSS', () => {
describe('with default raws', () => {
it('with no arguments', () =>
expect(new ContentRule().toString()).toBe('@content'));

it('with an argument', () =>
expect(
new ContentRule({contentArguments: [{text: 'bar'}]}).toString(),
).toBe('@content(bar)'));
});

it('with afterName', () =>
expect(
new ContentRule({
contentArguments: [{text: 'bar'}],
raws: {afterName: '/**/'},
}).toString(),
).toBe('@content/**/(bar)'));

it('with showArguments = true', () =>
expect(new ContentRule({raws: {showArguments: true}}).toString()).toBe(
'@content()',
));

it('ignores showArguments with an argument', () =>
expect(
new ContentRule({
contentArguments: [{text: 'bar'}],
raws: {showArguments: false},
}).toString(),
).toBe('@content(bar)'));
});
});

describe('clone', () => {
let original: ContentRule;
beforeEach(() => {
original = (
scss.parse('@mixin foo {@content(bar)}').nodes[0] as MixinRule
).nodes[0] as ContentRule;
// TODO: remove this once raws are properly parsed
original.raws.afterName = ' ';
});

describe('with no overrides', () => {
let clone: ContentRule;
beforeEach(() => void (clone = original.clone()));

describe('has the same properties:', () => {
it('params', () => expect(clone.params).toBe('(bar)'));

it('contentArguments', () => {
expect(clone.contentArguments.nodes[0]).toHaveStringExpression(
'value',
'bar',
);
expect(clone.contentArguments.parent).toBe(clone);
});

it('raws', () => expect(clone.raws).toEqual({afterName: ' '}));

it('source', () => expect(clone.source).toBe(original.source));
});

describe('creates a new', () => {
it('self', () => expect(clone).not.toBe(original));

for (const attr of ['contentArguments', 'raws'] as const) {
it(attr, () => expect(clone[attr]).not.toBe(original[attr]));
}
});
});

describe('overrides', () => {
describe('raws', () => {
it('defined', () =>
expect(original.clone({raws: {showArguments: true}}).raws).toEqual({
showArguments: true,
}));

it('undefined', () =>
expect(original.clone({raws: undefined}).raws).toEqual({
afterName: ' ',
}));
});

describe('contentArguments', () => {
describe('defined', () => {
let clone: ContentRule;
beforeEach(() => {
clone = original.clone({contentArguments: [{text: 'qux'}]});
});

it('changes params', () => expect(clone.params).toBe('(qux)'));

it('changes arguments', () => {
expect(clone.contentArguments.nodes[0]).toHaveStringExpression(
'value',
'qux',
);
expect(clone.contentArguments.parent).toBe(clone);
});
});

describe('undefined', () => {
let clone: ContentRule;
beforeEach(() => {
clone = original.clone({contentArguments: undefined});
});

it('preserves params', () => expect(clone.params).toBe('(bar)'));

it('preserves arguments', () =>
expect(clone.contentArguments.nodes[0]).toHaveStringExpression(
'value',
'bar',
));
});
});
});
});

it('toJSON', () =>
expect(
(scss.parse('@mixin foo {@content(bar)}').nodes[0] as MixinRule).nodes[0],
).toMatchSnapshot());
});
Loading

0 comments on commit aed7839

Please sign in to comment.