aboutsummaryrefslogtreecommitdiffstats
path: root/src/bem
diff options
context:
space:
mode:
authorFeuerfuchs <git@feuerfuchs.dev>2020-11-01 20:55:14 +0100
committerFeuerfuchs <git@feuerfuchs.dev>2020-11-01 20:55:14 +0100
commitd07f664450ddaaebb44127a4bd057763d13d3f82 (patch)
tree234cfd673ac527869a8dda4f32afbec48c87b512 /src/bem
downloadiro-sass-d07f664450ddaaebb44127a4bd057763d13d3f82.tar.gz
iro-sass-d07f664450ddaaebb44127a4bd057763d13d3f82.tar.bz2
iro-sass-d07f664450ddaaebb44127a4bd057763d13d3f82.zip
Init
Diffstat (limited to 'src/bem')
-rw-r--r--src/bem/_block.scss392
-rw-r--r--src/bem/_debug.scss16
-rw-r--r--src/bem/_element.scss622
-rw-r--r--src/bem/_functions.scss26
-rw-r--r--src/bem/_modifier.scss246
-rw-r--r--src/bem/_multi.scss131
-rw-r--r--src/bem/_state.scss146
-rw-r--r--src/bem/_suffix.scss118
-rw-r--r--src/bem/_theme.scss61
-rw-r--r--src/bem/_validators.scss176
-rw-r--r--src/bem/_vars.scss108
11 files changed, 2042 insertions, 0 deletions
diff --git a/src/bem/_block.scss b/src/bem/_block.scss
new file mode 100644
index 0000000..d065891
--- /dev/null
+++ b/src/bem/_block.scss
@@ -0,0 +1,392 @@
1////
2/// @group BEM
3///
4/// @access public
5////
6
7///
8/// Generate a new block.
9///
10/// This mixin simply creates a new block with the name {namespace}_{name},
11/// where {namespace} is the prefix assigned to $type and {name} is the
12/// block's name.
13///
14/// @param {string} $name - Block name
15/// @param {string} $type [null] - BEMIT namespace of the block
16///
17/// @content
18///
19/// @throw If $type is invalid
20/// @throw If the block is preceded by another block, element, modifier or suffix
21///
22/// @example scss - Creating a new block
23/// @include iro-bem-block('something', 'component') {
24/// /* some definitions */
25/// }
26///
27/// // Generates:
28///
29/// .c-something {
30/// /* some definitions */
31/// }
32///
33@mixin iro-bem-block($name, $type: null) {
34 $result: iro-bem-block($name, $type);
35 $selector: nth($result, 1);
36 $context: nth($result, 2);
37
38 @include iro-bem-validate(
39 'block',
40 (name: $name, type: $type),
41 $selector,
42 $context
43 );
44
45 @if $type != null {
46 $iro-bem-blocks: append($iro-bem-blocks, $name + '_' + $type) !global;
47 } @else {
48 $iro-bem-blocks: append($iro-bem-blocks, $name) !global;
49 }
50
51 @include iro-context-push($iro-bem-context-id, $context...);
52 @at-root #{$selector} {
53 @content;
54 }
55 @include iro-context-pop($iro-bem-context-id);
56}
57
58///
59/// Generate a new block. Check the respective mixin documentation for more information.
60///
61/// @return {list} A list with two items: 1. selector, 2. context or `null`
62///
63/// @see {mixin} iro-bem-block
64///
65@function iro-bem-block($name, $type: null) {
66 //
67 // Possible outcomes:
68 // - ({b,e,m,s}) block
69 //
70
71 $noop: iro-context-assert-stack-count($iro-bem-context-id, $iro-bem-max-depth);
72
73 $selector: null;
74 $base-selector: null;
75
76 @if $type != null {
77 $namespace: map-get($iro-bem-namespaces, $type);
78
79 @if not $namespace {
80 @error '"#{$type}" is not a valid type.';
81 }
82
83 $base-selector: selector-parse('.' + $namespace + '-' + $name);
84
85 @if $type != 'theme' or & {
86 $selector: $base-selector;
87 } @else if not & {
88 $selector: iro-bem-theme-selector($name);
89 }
90 } @else {
91 $base-selector: selector-parse('.' + $name);
92 $selector: $base-selector;
93 }
94
95 @if & {
96 $selector: selector-nest(&, $selector);
97 }
98
99 $context: 'block', (
100 'name': $name,
101 'type': $type,
102 'selector': $selector,
103 'base-selector': $base-selector
104 );
105
106 @return $selector $context;
107}
108
109///
110/// Generate a new object block. It's a shorthand for iro-bem-block($name, 'object').
111///
112/// @param {string} $name - Object block name
113///
114/// @content
115///
116@mixin iro-bem-object($name) {
117 @include iro-bem-block($name, 'object') {
118 @content;
119 }
120}
121
122///
123/// Generate a new object block. Check the respective mixin documentation for more information.
124///
125/// @return {list} A list with two items: 1. selector, 2. context or `null`
126///
127/// @see {mixin} iro-bem-object
128///
129@function iro-bem-object($name) {
130 @return iro-bem-block($name, 'object');
131}
132
133///
134/// Generate a new component block. It's a shorthand for iro-bem-block($name, 'component').
135///
136/// @param {string} $name - Component block name
137///
138/// @content
139///
140@mixin iro-bem-component($name) {
141 @include iro-bem-block($name, 'component') {
142 @content;
143 }
144}
145
146///
147/// Generate a new component block. Check the respective mixin documentation for more information.
148///
149/// @return {list} A list with two items: 1. selector, 2. context or `null`
150///
151/// @see {mixin} iro-bem-component
152///
153@function iro-bem-component($name) {
154 @return iro-bem-block($name, 'component');
155}
156
157///
158/// Generate a new layout block. It's a shorthand for iro-bem-block($name, 'layout').
159///
160/// @param {string} $name - Layout block name
161///
162/// @content
163///
164@mixin iro-bem-layout($name) {
165 @include iro-bem-block($name, 'layout') {
166 @content;
167 }
168}
169
170///
171/// Generate a new layout block. Check the respective mixin documentation for more information.
172///
173/// @return {list} A list with two items: 1. selector, 2. context or `null`
174///
175/// @see {mixin} iro-bem-layout
176///
177@function iro-bem-layout($name) {
178 @return iro-bem-block($name, 'layout');
179}
180
181///
182/// Generate a new utility block. It's a shorthand for iro-bem-block($name, 'utility').
183///
184/// @param {string} $name - Utility block name
185///
186/// @content
187///
188@mixin iro-bem-utility($name) {
189 @include iro-bem-block($name, 'utility') {
190 @content;
191 }
192}
193
194///
195/// Generate a new utility block. Check the respective mixin documentation for more information.
196///
197/// @return {list} A list with two items: 1. selector, 2. context or `null`
198///
199/// @see {mixin} iro-bem-utility
200///
201@function iro-bem-utility($name) {
202 @return iro-bem-block($name, 'utility');
203}
204
205///
206/// Generate a new scope block. It's a shorthand for iro-bem-block($name, 'scope').
207///
208/// @param {string} $name - Scope block name
209///
210/// @content
211///
212@mixin iro-bem-scope($name) {
213 @include iro-bem-block($name, 'scope') {
214 @content;
215 }
216}
217
218///
219/// Generate a new scope block. Check the respective mixin documentation for more information.
220///
221/// @return {list} A list with two items: 1. selector, 2. context or `null`
222///
223/// @see {mixin} iro-bem-scope
224///
225@function iro-bem-scope($name) {
226 @return iro-bem-block($name, 'scope');
227}
228
229///
230/// Generate a new theme block. It's a shorthand for iro-bem-block($name, 'theme').
231///
232/// @param {string} $name - Theme block name
233///
234/// @content
235///
236@mixin iro-bem-theme($name) {
237 @include iro-bem-block($name, 'theme') {
238 @content;
239 }
240}
241
242///
243/// Generate a new theme block. Check the respective mixin documentation for more information.
244///
245/// @return {list} A list with two items: 1. selector, 2. context or `null`
246///
247/// @see {mixin} iro-bem-theme
248///
249@function iro-bem-theme($name) {
250 @return iro-bem-block($name, 'theme');
251}
252
253///
254/// Generate a new JS block. It's a shorthand for iro-bem-block($name, 'js').
255///
256/// @param {string} $name - JS block name
257///
258/// @content
259///
260@mixin iro-bem-js($name) {
261 @include iro-bem-block($name, 'js') {
262 @content;
263 }
264}
265
266///
267/// Generate a new JS block. Check the respective mixin documentation for more information.
268///
269/// @return {list} A list with two items: 1. selector, 2. context or `null`
270///
271/// @see {mixin} iro-bem-js
272///
273@function iro-bem-js($name) {
274 @return iro-bem-block($name, 'js');
275}
276
277///
278/// Generate a new QA block. It's a shorthand for iro-bem-block($name, 'qa').
279///
280/// @param {string} $name - QA block name
281///
282/// @content
283///
284@mixin iro-bem-qa($name) {
285 @include iro-bem-block($name, 'qa') {
286 @content;
287 }
288}
289
290///
291/// Generate a new QA block. Check the respective mixin documentation for more information.
292///
293/// @return {list} A list with two items: 1. selector, 2. context or `null`
294///
295/// @see {mixin} iro-bem-qa
296///
297@function iro-bem-qa($name) {
298 @return iro-bem-block($name, 'qa');
299}
300
301///
302/// Generate a new hack block. It's a shorthand for iro-bem-block($name, 'hack').
303///
304/// @param {string} $name - Hack block name
305///
306/// @content
307///
308@mixin iro-bem-hack($name) {
309 @include iro-bem-block($name, 'hack') {
310 @content;
311 }
312}
313
314///
315/// Generate a new hack block. Check the respective mixin documentation for more information.
316///
317/// @return {list} A list with two items: 1. selector, 2. context or `null`
318///
319/// @see {mixin} iro-bem-hack
320///
321@function iro-bem-hack($name) {
322 @return iro-bem-block($name, 'hack');
323}
324
325///
326/// Assert that a block or element is composed of another block. In BEM, such a relationship is referred to
327/// as a "mix": https://en.bem.info/methodology/key-concepts/#mix
328///
329/// Compilation will fail if the foreign block doesn't exist. This way, you can ensure that blocks are
330/// defined in the right order so that composed blocks/elements will actually override the foreign
331/// declarations without having to artificially increase the specificity.
332///
333/// @param {string | list} $block - Either first block name, or list with two items: 1. block name, 2. block type
334/// @param {string | list} $blocks - Either other block names, or list with two items: 1. block name, 2. block type
335///
336/// @throw If a block type is invalid
337/// @throw If a block doesn't exist
338///
339/// @example scss - Successful assertion
340/// @include iro-bem-component('someBlock') {
341/// /* some definitions */
342/// }
343///
344/// @include iro-bem-component('anotherBlock') {
345/// /* some definitions */
346///
347/// @include iro-bem-element('elem') {
348/// @include iro-bem-composed-of('someBlock' 'component');
349///
350/// /* some definitions */
351/// }
352/// }
353///
354/// // Intended use: <div class="c-anotherBlock__elem c-someBlock">...</div>
355///
356/// @example scss - Failing assertion
357/// @include iro-bem-component('anotherBlock') {
358/// /* some definitions */
359///
360/// @include iro-bem-element('elem') {
361/// @include iro-bem-composed-of('someBlock' 'component');
362///
363/// /* some definitions */
364/// }
365/// }
366///
367/// @include iro-bem-component('someBlock') {
368/// /* some definitions */
369/// }
370///
371/// // Compilation will fail because c-someBlock is defined after c-anotherBlock__elem
372///
373@mixin iro-bem-composed-of($block, $blocks...) {
374 @each $block in iro-list-prepend($blocks, $block) {
375 @if type-of($block) == string {
376 @if not index($iro-bem-blocks, $block) {
377 @error 'Block "#{$block}" does not exist.';
378 }
379 } @else {
380 $name: nth($block, 1);
381 $type: nth($block, 2);
382
383 @if not map-get($iro-bem-namespaces, $type) {
384 @error '"#{$type}" is not a valid type.';
385 }
386
387 @if not index($iro-bem-blocks, $name + '_' + $type) {
388 @error 'Block "#{$name}" does not exist.';
389 }
390 }
391 }
392}
diff --git a/src/bem/_debug.scss b/src/bem/_debug.scss
new file mode 100644
index 0000000..e69083c
--- /dev/null
+++ b/src/bem/_debug.scss
@@ -0,0 +1,16 @@
1////
2/// @group BEM
3///
4/// @access public
5////
6
7@if $iro-bem-debug {
8 @each $type, $color in $iro-bem-debug-colors {
9 $namespace: map-get($iro-bem-namespaces, $type);
10
11 [class^='#{$namespace}-'],
12 [class*=' #{$namespace}-'] {
13 outline: 5px solid $color;
14 }
15 }
16}
diff --git a/src/bem/_element.scss b/src/bem/_element.scss
new file mode 100644
index 0000000..b3d2fee
--- /dev/null
+++ b/src/bem/_element.scss
@@ -0,0 +1,622 @@
1////
2/// @group BEM
3///
4/// @access public
5////
6
7///
8/// Generate a new BEM element.
9///
10/// The element will be generated according to the BEM naming convention.
11/// If the parent selector doesn't match the block selector, the element will be
12/// nested inside the parent selector. This means, you may nest elements inside
13/// other elements, modifiers or any kind of selector such as &:hover.
14///
15/// @param {string} $name - First element name
16/// @param {string} $names - More element names
17///
18/// @content
19///
20/// @throw If the element is not preceded by a block, element, modifier or suffix.
21///
22/// @example scss - Element for a block
23/// @include iro-bem-component('block') {
24/// /* some block definitions */
25///
26/// @include iro-bem-element('elem') {
27/// /* some element definitions */
28/// }
29/// }
30///
31/// // Generates:
32///
33/// .c-block {
34/// /* some block definitions */
35/// }
36///
37/// .c-block__elem {
38/// /* some element definitions */
39/// }
40///
41/// @example scss - Element that is affected by the user hovering the block
42/// @include iro-bem-component('block') {
43/// /* some block definitions */
44///
45/// @include iro-bem-element('elem') {
46/// background-color: #eee;
47/// }
48///
49/// &:hover {
50/// @include iro-bem-element('elem') {
51/// background-color: #000;
52/// }
53/// }
54/// }
55///
56/// // Generates:
57///
58/// .c-block {
59/// /* some block definitions */
60/// }
61///
62/// .c-block__elem {
63/// background-color: #eee;
64/// }
65///
66/// .c-block:hover .c-block__elem {
67/// background-color: #000;
68/// }
69///
70/// @example scss - Multiple elements
71/// @include iro-bem-component('block') {
72/// /* some block definitions */
73///
74/// @include iro-bem-element('elem1', 'elem2') {
75/// /* some element definitions */
76/// }
77/// }
78///
79/// // Generates:
80///
81/// .c-block {
82/// /* some block definitions */
83/// }
84///
85/// .c-block__elem1, .c-block__elem2 {
86/// /* some element definitions */
87/// }
88///
89@mixin iro-bem-element($name, $names...) {
90 $result: iro-bem-element($name, $names...);
91 $selector: nth($result, 1);
92 $context: nth($result, 2);
93
94 @include iro-bem-validate(
95 'element',
96 (name: $name, names: $names),
97 $selector,
98 $context
99 );
100
101 @include iro-context-push($iro-bem-context-id, $context...);
102 @at-root #{$selector} {
103 @content;
104 }
105 @include iro-context-pop($iro-bem-context-id);
106}
107
108///
109/// Generate a new BEM element. Check the respective mixin documentation for more information.
110///
111/// @return {list} A list with two items: 1. selector, 2. context or `null`
112///
113/// @see {mixin} iro-bem-element
114///
115@function iro-bem-element($name, $names...) {
116 $noop: iro-context-assert-stack-count($iro-bem-context-id, $iro-bem-max-depth);
117 $noop: iro-context-assert-stack-must-contain($iro-bem-context-id, 'block');
118
119 $parent-context: iro-context-get($iro-bem-context-id, 'block' 'element');
120
121 $selector: ();
122 $parts-data: ();
123
124 @if nth($parent-context, 1) == 'element' {
125 @if $iro-bem-element-nesting-policy == 'disallow' {
126 @error 'Element nesting is forbidden.';
127 }
128
129 @if $iro-bem-element-nesting-policy == 'append' {
130 $element-selector: map-get(nth($parent-context, 2), 'selector');
131
132 @if not iro-selector-suffix-match(&, $element-selector) {
133 @error 'A nested element must be an immediate children of the parent element.';
134 }
135
136 //
137 // Possible outcomes:
138 // - {e}__element
139 // - [manual selector] {e}__element
140 //
141
142 @each $name in join($name, $names) {
143 $sel: selector-append(&, $iro-bem-element-separator + $name);
144 $selector: join($selector, $sel, comma);
145 $parts-data: append($parts-data, (
146 'name': $name,
147 'selector': $sel
148 ));
149 }
150 }
151
152 $parent-context: iro-context-get($iro-bem-context-id, 'block');
153 }
154
155 @if length($selector) == 0 {
156 $parent-selector: map-get(nth($parent-context, 2), 'selector');
157
158 @if iro-selector-suffix-match(&, $parent-selector) {
159 //
160 // Possible outcomes:
161 // - {b}__element
162 // - [manual selector] {b}__element
163 //
164
165 @each $name in join($name, $names) {
166 $sel: selector-append(&, $iro-bem-element-separator + $name);
167 $selector: join($selector, $sel, comma);
168 $parts-data: append($parts-data, (
169 'name': $name,
170 'selector': $sel
171 ));
172 }
173 } @else {
174 //
175 // Possible outcomes:
176 // - {b} [manual selector] {b}__element
177 // - {e,m,s} ([manual selector]) {b}__element
178 //
179
180 @if nth($parent-context, 1) != 'block' {
181 $parent-context: iro-context-get($iro-bem-context-id, 'block');
182 }
183
184 $block-base-selector: map-get(nth($parent-context, 2), 'base-selector');
185
186 @each $name in join($name, $names) {
187 $sel: selector-nest(&, selector-append($block-base-selector, $iro-bem-element-separator + $name));
188 $selector: join($selector, $sel, comma);
189 $parts-data: append($parts-data, (
190 'name': $name,
191 'selector': $sel
192 ));
193 }
194 }
195 }
196
197 $context: 'element', (
198 'parts': $parts-data,
199 'selector': $selector
200 );
201
202 @return $selector $context;
203}
204
205///
206/// Generate a BEM element that is related to the current element.
207///
208/// The generated element selector is appended to the current element selector. The $sign
209/// determines the relationship.
210///
211/// @param {string} $sign - Relationshop sign, either '+' or '~'
212/// @param {string} $name - First element name
213/// @param {string} $names - More element names
214///
215/// @content
216///
217/// @throw If the element is not preceded by an element.
218///
219/// @example scss - A sibling element to a single element
220/// @include iro-bem-component('block') {
221/// @include iro-bem-element('elem') {
222/// /* some element definitions */
223///
224/// @include iro-bem-related-element('~', 'sibling') {
225/// /* some sibling element definitions */
226/// }
227/// }
228/// }
229///
230/// // Generates:
231///
232/// .c-block__elem {
233/// /* some element definitions */
234/// }
235///
236/// .c-block__elem ~ .c-block__sibling {
237/// /* some sibling element definitions */
238/// }
239///
240/// @example scss - A successor element to a single element
241/// @include iro-bem-component('block') {
242/// @include iro-bem-element('elem') {
243/// /* some element definitions */
244///
245/// @include iro-bem-related-element('+', 'successor') {
246/// /* some successor element definitions */
247/// }
248/// }
249/// }
250///
251/// // Generates:
252///
253/// .c-block__elem {
254/// /* some element definitions */
255/// }
256///
257/// .c-block__elem + .c-block__successor {
258/// /* some successor element definitions */
259/// }
260///
261/// @example scss - A successor element to multiple elements
262/// @include iro-bem-component('block') {
263/// @include iro-bem-element('elem1', 'elem2') {
264/// /* some element definitions */
265///
266/// @include iro-bem-related-element('+', 'successor') {
267/// /* some successor element definitions */
268/// }
269/// }
270/// }
271///
272/// // Generates:
273///
274/// .c-block__elem1, .c-block__elem2 {
275/// /* some element definitions */
276/// }
277///
278/// .c-block__elem1 + .c-block__successor, .c-block__elem2 + .c-block__successor {
279/// /* some successor element definitions */
280/// }
281///
282@mixin iro-bem-related-element($sign, $name, $names...) {
283 $result: iro-bem-related-element($sign, $name, $names...);
284 $selector: nth($result, 1);
285 $context: nth($result, 2);
286
287 @include iro-bem-validate(
288 'related-element',
289 (sign: $sign, name: $name, names: $names),
290 $selector,
291 $context
292 );
293
294 @include iro-context-push($iro-bem-context-id, $context...);
295 @at-root #{$selector} {
296 @content;
297 }
298 @include iro-context-pop($iro-bem-context-id);
299}
300
301///
302/// Generate a new BEM element that is related to the current element.
303/// Check the respective mixin documentation for more information.
304///
305/// @return {list} A list with two items: 1. selector, 2. context or `null`
306///
307/// @see {mixin} iro-bem-related-element
308///
309@function iro-bem-related-element($sign, $name, $names...) {
310 //
311 // Generating this selector is simple: Take the latest block context, use it
312 // to generate the element part, and insert it at the end of the current selector.
313 // Possible outcomes:
314 // - {e} ({m,s}) ([manual selector]) + {e}
315 // - {e} ({m,s}) ([manual selector]) ~ {e}
316 //
317
318 $noop: iro-context-assert-stack-count($iro-bem-context-id, $iro-bem-max-depth);
319 $noop: iro-context-assert-stack-must-contain($iro-bem-context-id, 'element');
320
321 @if $sign != '+' and $sign != '~' {
322 @error 'Invalid relationship sign #{inspect($sign)}.';
323 }
324
325 $block-context: iro-context-get($iro-bem-context-id, 'block');
326 $block-base-selector: map-get(nth($block-context, 2), 'base-selector');
327
328 $selector: ();
329 $parts-data: ();
330
331 @each $name in join($name, $names) {
332 $sel: selector-nest(&, $sign, selector-append($block-base-selector, $iro-bem-element-separator + $name));
333 $selector: join($selector, $sel, comma);
334 $parts-data: append($parts-data, (
335 'name': $name,
336 'selector': $sel
337 ));
338 }
339
340 $context: 'element', (
341 'parts': $parts-data,
342 'selector': $selector
343 );
344
345 @return $selector $context;
346}
347
348///
349/// Generate a BEM element that is a sibling of the current element.
350///
351/// It's a shorthand for iro-bem-related-element('~', $name).
352///
353/// @param {string} $name - First element name
354/// @param {list} $names - List of more element names
355///
356/// @content
357///
358@mixin iro-bem-sibling-element($name, $names...) {
359 @include iro-bem-related-element('~', $name, $names...) {
360 @content;
361 }
362}
363
364///
365/// Generate a new BEM element that is a sibling of the current element.
366/// Check the respective mixin documentation for more information.
367///
368/// @return {list} A list with two items: 1. selector, 2. context or `null`
369///
370/// @see {mixin} iro-bem-sibling-element
371///
372@function iro-bem-sibling-element($name, $names...) {
373 @return iro-bem-related-element('~', $name, $names...);
374}
375
376///
377/// Generate a BEM element that is the successor of the current element.
378///
379/// It's a shorthand for iro-bem-related-element('+', $name).
380///
381/// @param {string} $name - First element name
382/// @param {string} $names - More element names
383///
384/// @content
385///
386@mixin iro-bem-next-element($name, $names...) {
387 @include iro-bem-related-element('+', $name, $names...) {
388 @content;
389 }
390}
391
392///
393/// Generate a new BEM element that is the successor of the current element.
394/// Check the respective mixin documentation for more information.
395///
396/// @return {list} A list with two items: 1. selector, 2. context or `null`
397///
398/// @see {mixin} iro-bem-next-element
399///
400@function iro-bem-next-element($name, $names...) {
401 @return iro-bem-related-element('+', $name, $names...);
402}
403
404///
405/// Generate the current BEM element as a successor of itself.
406///
407/// If this is applied to a single element, it behaves exactly the same as
408/// iro-bem-related-element('+', name);
409/// However, if it is applied to multiple elements, each twin element only will influence
410/// their other twin, which is not replicable with iro-bem-related-element.
411///
412/// @content
413///
414/// @example scss - Two twin elements
415/// @include iro-bem-component('block') {
416/// @include iro-bem-element('elem') {
417/// /* some element definitions */
418///
419/// @include iro-bem-next-twin-element {
420/// /* some twin element definitions */
421/// }
422/// }
423/// }
424///
425/// // Generates:
426///
427/// .c-block__elem {
428/// /* some element definitions */
429/// }
430///
431/// .c-block__elem + .c-block__elem {
432/// /* some twin element definitions */
433/// }
434///
435/// @example scss - Multiple twin elements
436/// @include iro-bem-component('block') {
437/// @include iro-bem-element('elem1', 'elem2') {
438/// /* some element definitions */
439///
440/// @include iro-bem-next-twin-element {
441/// /* some twin element definitions */
442/// }
443/// }
444/// }
445///
446/// // Generates:
447///
448/// .c-block__elem1, .c-block__elem2 {
449/// /* some element definitions */
450/// }
451///
452/// .c-block__elem1 + .c-block__elem1, .c-block__elem2 + .c-block__elem2 {
453/// /* some twin element definitions */
454/// }
455///
456@mixin iro-bem-related-twin-element($sign) {
457 $result: iro-bem-related-twin-element($sign);
458 $selector: nth($result, 1);
459 $context: nth($result, 2);
460
461 @include iro-bem-validate(
462 'next-twin-element',
463 (),
464 $selector,
465 $context
466 );
467
468 @include iro-context-push($iro-bem-context-id, $context...);
469 @at-root #{$selector} {
470 @content;
471 }
472 @include iro-context-pop($iro-bem-context-id);
473}
474
475///
476/// Generate the current BEM element as a successor of itself.
477/// Check the respective mixin documentation for more information.
478///
479/// @return {list} A list with two items: 1. selector, 2. context or `null`
480///
481/// @see {mixin} iro-bem-next-twin-element
482///
483@function iro-bem-related-twin-element($sign) {
484 $noop: iro-context-assert-stack-count($iro-bem-context-id, $iro-bem-max-depth);
485 $noop: iro-context-assert-stack-must-contain($iro-bem-context-id, 'element');
486
487 $element-context: iro-context-get($iro-bem-context-id, 'element');
488 $element-selector: map-get(nth($element-context, 2), 'selector');
489
490 $block-context: iro-context-get($iro-bem-context-id, 'block');
491 $block-base-selector: map-get(nth($block-context, 2), 'base-selector');
492
493 $selector: ();
494 $parts-data: ();
495
496 //
497 // To determine the twin for each element, iterate the sub-selectors from the current selector
498 // and check if it contains the currently inspected element. This has to be done with string
499 // comparison since none of Sass selector functions is of use here.
500 // Finally, the current twin will be appended to the extracted sub-selector as a successor
501 // element.
502 //
503 @each $part-data in map-get(nth($element-context, 2), 'parts') {
504 $part-selector: map-get($part-data, 'selector');
505 $part-name: map-get($part-data, 'name');
506
507 $sel: ();
508 @if iro-selector-suffix-match(&, $element-selector) {
509 //
510 // This mixin is included in the selector the last element mixin created.
511 // Possible outcomes:
512 // - {e} + {e}
513 // - [manual selector] {e} + {e}
514 //
515
516 @each $s in & {
517 @each $ps in $part-selector {
518 @if nth($s, -1) == nth($ps, -1) {
519 $sel-ent: selector-nest($s, $sign, selector-append($block-base-selector, $iro-bem-element-separator + $part-name));
520 $sel: join($sel, $sel-ent, comma);
521 }
522 }
523 }
524 } @else {
525 //
526 // This mixin is NOT included in the selector the last element mixin created.
527 // Possible outcomes:
528 // - {e} {m,s} + {e}
529 // - {e} [manual selector] + {e}
530 // - {e} {m,s} [manual selector] + {e}
531 //
532
533 @each $s in & {
534 @each $ps in $part-selector {
535 @if str-index(inspect($s), inspect($ps)) {
536 $char-index: str-length(inspect($ps)) + 1;
537 $match: index(' ' ':' ',', str-slice(inspect($s), $char-index, $char-index)) != null;
538
539 @if not $match {
540 @each $separator in $iro-bem-element-separator $iro-bem-modifier-separator $iro-bem-suffix-separator {
541 @if str-slice(inspect($s), $char-index, $char-index + str-length($separator) - 1) == $separator {
542 $match: true;
543 }
544 }
545 }
546
547 @if $match {
548 $sel-ent: selector-nest($s, '+', selector-append($block-base-selector, $iro-bem-element-separator + $part-name));
549 $sel: join($sel, $sel-ent, comma);
550 }
551 }
552 }
553 }
554 }
555 @if length($sel) != length($part-selector) {
556 @error 'Could not generate twin element selector.';
557 }
558
559 $selector: join($selector, $sel, comma);
560 $parts-data: append($parts-data, (
561 'name': $part-name,
562 'selector': $sel
563 ));
564 }
565
566 $context: 'element', (
567 'parts': $parts-data,
568 'selector': $selector
569 );
570
571 @return $selector $context;
572}
573
574///
575/// Generate the current BEM element as a sibling of itself.
576///
577/// It's a shorthand for iro-bem-related-twin-element('~').
578///
579/// @content
580///
581@mixin iro-bem-sibling-twin-element {
582 @include iro-bem-related-twin-element('~') {
583 @content;
584 }
585}
586
587///
588/// Generate the current BEM element as a sibling of itself.
589/// Check the respective mixin documentation for more information.
590///
591/// @return {list} A list with two items: 1. selector, 2. context or `null`
592///
593/// @see {mixin} iro-bem-sibling-twin-element
594///
595@function iro-bem-sibling-twin-element() {
596 @return iro-bem-related-twin-element('~');
597}
598
599///
600/// Generate the current BEM element as a next sibling of itself.
601///
602/// It's a shorthand for iro-bem-related-twin-element('+', $name).
603///
604/// @content
605///
606@mixin iro-bem-next-twin-element {
607 @include iro-bem-related-twin-element('+') {
608 @content;
609 }
610}
611
612///
613/// Generate the current BEM element as a next sibling of itself.
614/// Check the respective mixin documentation for more information.
615///
616/// @return {list} A list with two items: 1. selector, 2. context or `null`
617///
618/// @see {mixin} iro-bem-next-twin-element
619///
620@function iro-bem-next-twin-element() {
621 @return iro-bem-related-twin-element('+');
622}
diff --git a/src/bem/_functions.scss b/src/bem/_functions.scss
new file mode 100644
index 0000000..4bb95c4
--- /dev/null
+++ b/src/bem/_functions.scss
@@ -0,0 +1,26 @@
1////
2/// @group BEM
3///
4/// @access public
5////
6
7///
8/// @access private
9///
10@function iro-bem-theme-selector($name, $names...) {
11 $namespace: map-get($iro-bem-namespaces, 'theme');
12 $selector: null;
13
14 @each $name in join($name, $names) {
15 $sel: '.' + $namespace + '-' + $name;
16
17 @if $selector == null {
18 $selector: join(selector-parse($sel), selector-parse('[class*=\' t-\'] ' + $sel), comma);
19 $selector: join($selector, selector-parse('[class^=\'t-\'] ' + $sel), comma);
20 } @else {
21 $selector: selector-nest($selector, $sel);
22 }
23 }
24
25 @return $selector;
26}
diff --git a/src/bem/_modifier.scss b/src/bem/_modifier.scss
new file mode 100644
index 0000000..e1f9507
--- /dev/null
+++ b/src/bem/_modifier.scss
@@ -0,0 +1,246 @@
1////
2/// @group BEM
3///
4/// @access public
5////
6
7///
8/// Generate a new BEM modifier.
9///
10/// If the parent context is block or element, the modifier will modify said block or element according
11/// to the BEM naming convention.
12///
13/// If the parent context is a modifier or suffix, then the modifier will depend on said modifier or suffix.
14/// Depending on $extend, the meaning of this dependency (and the resulting selector) varies:
15/// If it's false (default), you signalize that the modifier also exists by itself, but it changes its
16/// behavior when the parent modifier or suffix is set.
17/// If it's true, you signalize that the modifier extends the parent modifier or suffix and can only be
18/// used in conjunction with it.
19///
20/// @param {string | list} $name - First element name or list with two items: 1. first element name, 2. bool indicating if the modifier is extending
21/// @param {string | list} $names - More element names or lists with two items: 1. element name, 2. bool indicating if the modifier is extending
22///
23/// @content
24///
25/// @throw If the element is not preceded by a block, element, modifier or suffix.
26///
27/// @example scss - Modifier that modifies a block or element
28/// @include iro-bem-component('block') {
29/// @include iro-bem-modifier('mod') {
30/// background-color: #eee;
31/// }
32///
33/// @include iro-bem-element('elem') {
34/// @include iro-bem-modifier('mod') {
35/// background-color: #222;
36/// }
37/// }
38/// }
39///
40/// // Generates:
41///
42/// .c-block--mod {
43/// background-color: #eee;
44/// }
45///
46/// .c-block__elem--mod {
47/// background-color: #222;
48/// }
49///
50/// @example scss - Modifier nested in another modifier, not extending
51/// @include iro-bem-component('block') {
52/// @include iro-bem-modifier('mod') {
53/// background-color: #eee;
54/// }
55///
56/// @include iro-bem-modifier('dark') {
57/// /* some definitions */
58///
59/// @include iro-bem-modifier('mod') {
60/// background-color: #222;
61/// }
62/// }
63/// }
64///
65/// // Generates:
66///
67/// .c-block--mod {
68/// background-color: #eee;
69/// }
70///
71/// .c-block--dark {
72/// /* some definitions */
73/// }
74///
75/// .c-block--dark.c-block--mod {
76/// background-color: #222;
77/// }
78///
79/// @example scss - Modifier nested in another modifier, extending
80/// @include iro-bem-component('block') {
81/// @include iro-bem-modifier('mod') {
82/// background-color: #eee;
83/// }
84///
85/// @include iro-bem-modifier('dark') {
86/// /* some definitions */
87///
88/// @include iro-bem-modifier('mod' true) {
89/// background-color: #222;
90/// }
91/// }
92/// }
93///
94/// // Generates:
95///
96/// .c-block--mod {
97/// background-color: #eee;
98/// }
99///
100/// .c-block--dark {
101/// /* some definitions */
102/// }
103///
104/// .c-block--dark--mod {
105/// background-color: #222;
106/// }
107///
108@mixin iro-bem-modifier($name, $names...) {
109 $result: iro-bem-modifier($name, $names...);
110 $selector: nth($result, 1);
111 $context: nth($result, 2);
112
113 @include iro-bem-validate(
114 'modifier',
115 (name: $name, names: $names),
116 $selector,
117 $context
118 );
119
120 @include iro-context-push($iro-bem-context-id, $context...);
121 @at-root #{$selector} {
122 @content;
123 }
124 @include iro-context-pop($iro-bem-context-id);
125}
126
127///
128/// Generate a new BEM modifier. Check the respective mixin documentation for more information.
129///
130/// @return {list} A list with two items: 1. selector, 2. context or `null`
131///
132/// @see {mixin} iro-bem-modifier
133///
134@function iro-bem-modifier($name, $names...) {
135 $noop: iro-context-assert-stack-count($iro-bem-context-id, $iro-bem-max-depth);
136 $noop: iro-context-assert-stack-must-contain($iro-bem-context-id, 'block');
137
138 $parent-context: iro-context-get($iro-bem-context-id, 'block' 'element' 'modifier' 'suffix');
139 $parent-selector: map-get(nth($parent-context, 2), 'selector');
140 $selector: ();
141 $parts-data: ();
142
143 @if not iro-selector-suffix-match(&, $parent-selector) {
144 //
145 // The current selector doesn't match the parent selector.
146 // The user manually added a selector between parent context and this modifier call.
147 // This case is forbidden because any outcome semantically wouldn't make sense:
148 // - {b,e,m,s} [manual selector] {b,e,m,s}--modifier
149 // - {b,e,m,s}--modifier [manual selector]
150 // The first case would make the modifier behave like an element.
151 // The second case is unintuitive, the code would be more clear by nesting the manual
152 // selector in the modifier instead.
153 //
154
155 @error 'A modifier must be an immediate child of the parent context';
156 }
157
158 @each $name in iro-list-prepend($names, $name) {
159 $extend: false;
160 @if type-of($name) == list {
161 $extend: nth($name, 2);
162 $name: nth($name, 1);
163 }
164
165 @if index('block' 'element', nth($parent-context, 1)) or $extend == true {
166 //
167 // Either the parent context is block or element, or a modifier or suffix
168 // is to be extended. The modifier part can simply be appended.
169 // Possible outcomes:
170 // - {b,e,m,s}--modifier
171 //
172
173 $sel: selector-append(&, $iro-bem-modifier-separator + $name);
174 $selector: join($selector, $sel, comma);
175 $parts-data: append($parts-data, (
176 'name': $name,
177 'selector': $sel
178 ));
179 } @else {
180 //
181 // Parent context is modifier, suffix or state and $extend is false.
182 //
183
184 $be-context: iro-context-get($iro-bem-context-id, 'block' 'element');
185
186 @if nth($be-context, 1) == 'element' {
187 //
188 // Latest context is element. Since element contexts can consist of multiple single
189 // elements, inspect all elements and append its selector with the suffix "--$name".
190 // This has to be done with string comparison since none of Sass selector functions
191 // is of use here.
192 // Possible outcomes:
193 // - {m,s}.{e}--modifier
194 //
195
196 $nsel: ();
197
198 @each $elem-part-data in map-get(nth($be-context, 2), 'parts') {
199 $elem-part-selector: map-get($elem-part-data, 'selector');
200
201 $sel: ();
202 @each $s in & {
203 @each $ps in $elem-part-selector {
204 @if str-index(inspect($s), inspect($ps) + $iro-bem-modifier-separator) or str-index(inspect($s), inspect($ps) + $iro-bem-suffix-separator) {
205 $sel: join($sel, selector-unify($s, selector-append($ps, $iro-bem-modifier-separator + $name)), comma);
206 }
207 }
208 }
209 @if length($sel) == 0 {
210 @error 'Could not generate modifier selector.';
211 }
212
213 $nsel: join($nsel, $sel, comma);
214 }
215
216 $selector: join($selector, $nsel, comma);
217 $parts-data: append($parts-data, (
218 'name': $name,
219 'selector': $nsel
220 ));
221 } @else {
222 //
223 // Latest context is block. Just append the modifier part.
224 // Possible outcomes:
225 // - {m,s}.{b}--modifier
226 //
227
228 $block-base-selector: map-get(nth($be-context, 2), 'base-selector');
229
230 $sel: selector-append(&, $block-base-selector, $iro-bem-modifier-separator + $name);
231 $selector: join($selector, $sel, comma);
232 $parts-data: append($parts-data, (
233 'name': $name,
234 'selector': $sel
235 ));
236 }
237 }
238 }
239
240 $context: 'modifier', (
241 'parts': $parts-data,
242 'selector': $selector
243 );
244
245 @return $selector $context;
246}
diff --git a/src/bem/_multi.scss b/src/bem/_multi.scss
new file mode 100644
index 0000000..9e47ce4
--- /dev/null
+++ b/src/bem/_multi.scss
@@ -0,0 +1,131 @@
1////
2/// @group BEM
3///
4/// @access public
5////
6
7///
8/// Generate multiple entities (BEM or not) at once.
9///
10/// NOTE: This mixin does not generate perfectly optimized selectors in order to keep track of contexts.
11///
12/// @param {string | list} $first - First selector. Either a string for a manual selector, or a list with the first items standing for a BEM selector function (optionally suffixed by a colon) and other items being passed as arguments to said function.
13/// @param {string | list} $others - Other selectors. Either a string for a manual selector, or a list with the first items standing for a BEM selector function (optionally suffixed by a colon) and other items being passed as arguments to said function.
14///
15/// @content
16///
17/// @example scss - Creating multiple elements, a modifier and an anchor
18/// @include iro-bem-object('buttonstrip') {
19/// display: none;
20///
21/// @include iro-bem-multi('modifier' 'mod', 'element' 'button' 'separator', '> a') {
22/// display: block;
23/// }
24/// }
25///
26/// // Generates:
27///
28/// .o-buttonstrip {
29/// display: none;
30/// }
31///
32/// .o-buttonstrip--mod {
33/// display: block;
34/// }
35///
36/// .o-buttonstrip__button, {
37/// .o-buttonstrip__separator {
38/// display: block;
39/// }
40///
41/// .o-buttonstrip > a {
42/// display: block;
43/// }
44///
45/// @example scss - Creating multiple elements, a modifier and an anchor - optional colons included
46/// @include iro-bem-object('buttonstrip') {
47/// display: none;
48///
49/// @include iro-bem-multi('modifier:' 'mod', 'element:' 'button' 'separator', '> a') {
50/// display: block;
51/// }
52/// }
53///
54/// // Generates:
55///
56/// .o-buttonstrip {
57/// display: none;
58/// }
59///
60/// .o-buttonstrip--mod {
61/// display: block;
62/// }
63///
64/// .o-buttonstrip__button, {
65/// .o-buttonstrip__separator {
66/// display: block;
67/// }
68///
69/// .o-buttonstrip > a {
70/// display: block;
71/// }
72///
73@mixin iro-bem-multi($first, $others...) {
74 @include iro-context-assert-stack-count($iro-bem-context-id, $iro-bem-max-depth);
75
76 @each $entity in iro-list-prepend($others, $first) {
77 $is-manual-selector: false;
78
79 @if type-of($entity) == string and not function-exists('iro-bem-' + $entity) {
80 $is-manual-selector: true;
81 }
82
83 @if $is-manual-selector {
84 $sel: if(&, selector-nest(&, $entity), selector-parse($entity));
85
86 @at-root #{$sel} {
87 @content;
88 }
89 } @else {
90 $entity-func-id: null;
91
92 @if type-of($entity) == list {
93 $entity-func-id: nth($entity, 1);
94 $entity: iro-list-slice($entity, 2);
95 } @else {
96 $entity-func-id: $entity;
97 $entity: ();
98 }
99
100 @if str-slice($entity-func-id, str-length($entity-func-id)) == ':' {
101 $entity-func-id: str-slice($entity-func-id, 1, str-length($entity-func-id) - 1);
102 }
103
104 $sel-func: null;
105
106 @if function-exists('iro-bem-' + $entity-func-id) {
107 $sel-func: get-function('iro-bem-' + $entity-func-id);
108 } @else if function-exists($entity-func-id) {
109 $sel-func: get-function($entity-func-id);
110 }
111
112 @if $sel-func == null {
113 @error 'Function "#{inspect($entity-func-id)}" was not found.';
114 }
115
116 $entity-result: call($sel-func, $entity...);
117 $entity-result-selector: nth($entity-result, 1);
118 $entity-result-context: nth($entity-result, 2);
119
120 @if $entity-result-context != null {
121 @include iro-context-push($iro-bem-context-id, $entity-result-context...);
122 }
123 @at-root #{$entity-result-selector} {
124 @content;
125 }
126 @if $entity-result-context != null {
127 @include iro-context-pop($iro-bem-context-id);
128 }
129 }
130 }
131}
diff --git a/src/bem/_state.scss b/src/bem/_state.scss
new file mode 100644
index 0000000..4a85bbb
--- /dev/null
+++ b/src/bem/_state.scss
@@ -0,0 +1,146 @@
1////
2/// @group BEM
3///
4/// @access public
5////
6
7///
8/// Create a new state rule.
9///
10/// @param {string} $state - First state name
11/// @param {list} $states - List of more state names
12///
13/// @content
14///
15/// @example scss - Using single is-state
16/// @include iro-bem-object('menu') {
17/// display: none;
18///
19/// @include iro-bem-state('is', open') {
20/// display: block;
21/// }
22/// }
23///
24/// // Generates:
25///
26/// .o-menu {
27/// display: none;
28/// }
29///
30/// .o-menu.is-open {
31/// display: block;
32/// }
33///
34/// @example scss - Using multiple is-states
35/// @include iro-bem-object('menu') {
36/// display: none;
37///
38/// @include iro-bem-state('is', open', 'visible') {
39/// display: block;
40/// }
41/// }
42///
43/// // Generates:
44///
45/// .o-menu {
46/// display: none;
47/// }
48///
49/// .o-menu.is-open,
50/// .o-menu.is-visible {
51/// display: block;
52/// }
53///
54@mixin iro-bem-state($prefix, $state, $states...) {
55 $result: iro-bem-state($prefix, $state, $states...);
56 $selector: nth($result, 1);
57 $context: nth($result, 2);
58
59 @include iro-bem-validate(
60 'state',
61 (prefix: $prefix, state: $state, states: $states),
62 $selector,
63 $context
64 );
65
66 @include iro-context-push($iro-bem-context-id, $context...);
67 @at-root #{$selector} {
68 @content;
69 }
70 @include iro-context-pop($iro-bem-context-id);
71}
72
73///
74/// Generate a new state.
75///
76/// @return {list} A list with two items: 1. selector, 2. context or `null`
77///
78/// @see {mixin} iro-bem-has
79///
80@function iro-bem-state($prefix, $state, $states...) {
81 $selector: ();
82 $parts-data: ();
83
84 @each $state in join($state, $states) {
85 $sel: selector-parse('.#{$prefix}-#{$state}');
86 @if & {
87 $sel: selector-append(&, $sel);
88 }
89 $selector: join($selector, $sel, comma);
90 $parts-data: append($parts-data, (
91 'name': $state,
92 'selector': $sel
93 ));
94 }
95
96 $context: 'state', (
97 'parts': $parts-data,
98 'selector': $selector
99 );
100
101 @return $selector $context;
102}
103
104///
105/// Create a new has-state modifier.
106///
107/// It's a shorthand for iro-bem-state('is', $state, $states...).
108///
109@mixin iro-bem-is($state, $states...) {
110 @include iro-bem-state('is', $state, $states...) {
111 @content;
112 }
113}
114
115///
116/// Generate a new is-state modifier. Check the respective mixin documentation for more information.
117///
118/// @return {list} A list with two items: 1. selector, 2. context or `null`
119///
120/// @see {mixin} iro-bem-is
121///
122@function iro-bem-is($state, $states...) {
123 @return iro-bem-state('is', $state, $states...);
124}
125
126///
127/// Create a new has-state modifier.
128///
129/// It's a shorthand for iro-bem-state('has', $state, $states...).
130///
131@mixin iro-bem-has($state, $states...) {
132 @include iro-bem-state('has', $state, $states...) {
133 @content;
134 }
135}
136
137///
138/// Generate a new has-state modifier. Check the respective mixin documentation for more information.
139///
140/// @return {list} A list with two items: 1. selector, 2. context or `null`
141///
142/// @see {mixin} iro-bem-has
143///
144@function iro-bem-has($state, $states...) {
145 @return iro-bem-state('has', $state, $states...);
146}
diff --git a/src/bem/_suffix.scss b/src/bem/_suffix.scss
new file mode 100644
index 0000000..b103c9f
--- /dev/null
+++ b/src/bem/_suffix.scss
@@ -0,0 +1,118 @@
1////
2/// @group BEM
3///
4/// @access public
5////
6
7///
8/// Generate a new suffix.
9///
10/// @param {string} $name - Suffix name
11///
12/// @content
13///
14/// @throw If the element is not preceded by a block or modifier.
15///
16/// @example scss - Using a suffix
17/// @include iro-bem-utility('hidden') {
18/// display: none;
19///
20/// @media (max-width: 320px) {
21/// @include iro-bem-suffix('phone') {
22/// display: none;
23/// }
24/// }
25///
26/// @media (max-width: 768px) {
27/// @include iro-bem-suffix('tablet') {
28/// display: none;
29/// }
30/// }
31/// }
32///
33/// // Generates:
34///
35/// .u-hidden {
36/// display: none;
37/// }
38///
39/// @media (max-width: 320px) {
40/// .u-hidden@phone {
41/// display: none;
42/// }
43/// }
44///
45/// @media (max-width: 768px) {
46/// .u-hidden@tablet {
47/// display: none;
48/// }
49/// }
50///
51@mixin iro-bem-suffix($name) {
52 $result: iro-bem-suffix($name);
53 $selector: nth($result, 1);
54 $context: nth($result, 2);
55
56 @include iro-bem-validate(
57 'suffix',
58 (name: $name),
59 $selector,
60 $context
61 );
62
63 @include iro-context-push($iro-bem-context-id, $context...);
64 @at-root #{$selector} {
65 @content;
66 }
67 @include iro-context-pop($iro-bem-context-id);
68}
69
70///
71/// Generate a new suffix. Check the respective mixin documentation for more information.
72///
73/// @return {list} A list with two items: 1. selector, 2. context or `null`
74///
75/// @see {mixin} iro-bem-suffix
76///
77@function iro-bem-suffix($name) {
78 //
79 // Suffixes can be used on block, element and modifier.
80 //
81
82 $noop: iro-context-assert-stack-count($iro-bem-context-id, $iro-bem-max-depth);
83 $noop: iro-context-assert-stack-must-contain($iro-bem-context-id, 'block');
84 $noop: iro-context-assert-stack-must-not-contain($iro-bem-context-id, 'suffix');
85
86 $parent-context: iro-context-get($iro-bem-context-id, 'block' 'element' 'modifier');
87 $parent-selector: map-get(nth($parent-context, 2), 'selector');
88
89 @if not iro-selector-suffix-match(&, $parent-selector) {
90 //
91 // The current selector doesn't match the parent selector.
92 // The user manually added a selector between parent context and this suffix call.
93 // This case is forbidden because any outcome semantically wouldn't make sense:
94 // - {b,e,m} [manual selector] {b,e,m}@suffix
95 // - {b,e,m}@suffix [manual selector]
96 // The first case would make the modifier behave like an element.
97 // The second case is unintuitive, the code would be more clear by nesting the manual
98 // selector in the suffix instead.
99 //
100
101 @error 'A suffix must be an immediate child of the parent context';
102 }
103
104 //
105 // The suffix part can simply be appended.
106 // Possible outcomes:
107 // - {b,e,m}@suffix
108 //
109
110 $selector: selector-append(&, $iro-bem-suffix-separator + $name);
111
112 $context: 'suffix', (
113 'name': $name,
114 'selector': $selector
115 );
116
117 @return $selector $context;
118}
diff --git a/src/bem/_theme.scss b/src/bem/_theme.scss
new file mode 100644
index 0000000..b4bcc76
--- /dev/null
+++ b/src/bem/_theme.scss
@@ -0,0 +1,61 @@
1////
2/// @group BEM
3///
4/// @access public
5////
6
7///
8/// Declare new rules for the current block for when this theme is active.
9///
10/// @param {string} $name - First theme block name
11/// @param {string} $names - More theme block names
12///
13/// @content
14///
15@mixin iro-bem-at-theme($name, $names...) {
16 $result: iro-bem-at-theme($name, $names...);
17 $selector: nth($result, 1);
18 $context: nth($result, 2);
19
20 @include iro-bem-validate(
21 'at-theme',
22 (name: $name, names: $names),
23 $selector,
24 $context
25 );
26
27 @include iro-context-push($iro-bem-context-id, $context...);
28 @at-root #{$selector} {
29 @content;
30 }
31 @include iro-context-pop($iro-bem-context-id);
32}
33
34///
35/// Declare new rules for the current block for when this theme is active.
36/// Check the respective mixin documentation for more information.
37///
38/// @return {list} A list with two items: 1. selector, 2. context or `null`
39///
40/// @see {mixin} iro-bem-at-theme
41///
42@function iro-bem-at-theme($name, $names...) {
43 $noop: iro-context-assert-stack-must-contain($iro-bem-context-id, 'block');
44
45 $parent-context: iro-context-get($iro-bem-context-id, 'block');
46 $parent-selector: map-get(nth($parent-context, 2), 'selector');
47
48 @if not iro-selector-suffix-match(&, $parent-selector) {
49 @error 'An at-theme rule must be an immediate child of a block';
50 }
51
52 $selector: iro-bem-theme-selector($name, $names...);
53 $selector: selector-nest($selector, &);
54
55 $context: 'at-theme', (
56 'name': join($name, $names),
57 'selector': $selector
58 );
59
60 @return $selector $context;
61}
diff --git a/src/bem/_validators.scss b/src/bem/_validators.scss
new file mode 100644
index 0000000..eb09a60
--- /dev/null
+++ b/src/bem/_validators.scss
@@ -0,0 +1,176 @@
1////
2/// Validators are custom functions that will be called before a BEM entity is created.
3/// They check if the current mixin usage is valid or not and thus they are a flexible way to
4/// let you implement your own rules.
5///
6/// Validator functions receive the following information:
7/// - BEM entity type
8/// - Arguments passed to the mixin
9/// - The generated selector
10/// - The generated context, if any
11///
12/// Additionally, the context stack used by the BEM system can be examined.
13///
14/// @group BEM
15///
16/// @access public
17////
18
19///
20/// A list of validator functions.
21///
22/// @type list
23///
24/// @access private
25///
26$iro-bem-validators: ();
27
28///
29/// Register one or multiple validator functions.
30///
31/// A validator function is a function that accepts 4 arguments:
32/// 1. BEM entity type (string)
33/// 2. Arguments passed to the mixin (map)
34/// 3. The generated selector (selector)
35/// 4. The generated context (list, may be null)
36///
37/// The function must return a list with two items:
38/// 1. `true` if the mixin usage is valid, otherwise `false`,
39/// 2. a string with a rejection reason (empty if the usage is valid).
40///
41/// @param {string} $func-name - First function name.
42/// @param {string} $func-names - Other function names.
43///
44@mixin iro-bem-add-validator($func-name, $func-names...) {
45 $noop: iro-bem-add-validator($func-name, $func-names...);
46}
47
48///
49/// Register one or multiple validator functions. Check the respective mixin documentation for more information.
50///
51/// @see {mixin} iro-bem-add-validator
52///
53@function iro-bem-add-validator($func-name, $func-names...) {
54 @each $fn-name in join($func-name, $func-names) {
55 $fn: get-function($fn-name);
56 $iro-bem-validators: map-merge($iro-bem-validators, ($fn-name: $fn)) !global;
57 }
58 @return null;
59}
60
61///
62/// Unregister one or multiple validator functions.
63///
64/// @param {string} $func-name - First function name.
65/// @param {string} $func-names - Other function names.
66///
67@mixin iro-bem-remove-validator($func-name, $func-names...) {
68 $noop: iro-bem-remove-validator($func-name, $func-names...);
69}
70
71///
72/// Unregister one or multiple validator functions. Check the respective mixin documentation for more information.
73///
74/// @see {mixin} iro-bem-remove-validator
75///
76@function iro-bem-remove-validator($func-name, $func-names...) {
77 $iro-bem-validators: map-remove($iro-bem-validators, $func-name, $func-names...) !global;
78 @return null;
79}
80
81///
82/// @access private
83///
84@mixin iro-bem-validate($type, $args, $selector, $context) {
85 @each $id, $fn in $iro-bem-validators {
86 $result: call($fn, $type, $args, $selector, $context);
87 @if not nth($result, 1) {
88 @error 'A BEM validator rejected this mixin usage due to the following reason: #{nth($result, 2)}';
89 }
90 }
91}
92
93//
94// ---------------------------------------------------------------------------------------------------------
95// Built-in validators
96// ---------------------------------------------------------------------------------------------------------
97//
98
99///
100/// A validator that makes sure blocks are declared in the right order, determined by the
101/// namespace used.
102///
103@function iro-bem-validator--enforce-namespace-order($type, $args, $selector, $context) {
104 @if not global-variable-exists(iro-bem-namespace-order) {
105 $iro-bem-namespace-order: map-keys($iro-bem-namespaces) !global;
106 }
107 @if not global-variable-exists(iro-bem-cur-namespace-index) {
108 $iro-bem-cur-namespace-index: 1 !global;
109 }
110
111 @if $type == 'block' {
112 $block-type: map-get($args, 'type');
113
114 @if $block-type != null {
115 $ns-index: index($iro-bem-namespace-order, $block-type);
116
117 @if $ns-index != null {
118 @if $ns-index < $iro-bem-cur-namespace-index {
119 @return false 'Namespace "#{$block-type}" comes before current namespace #{nth($iro-bem-namespace-order, $iro-bem-cur-namespace-index)}';
120 }
121
122 $iro-bem-cur-namespace-index: $ns-index !global;
123 }
124 }
125 }
126
127 @return true '';
128}
129
130///
131/// A validator that makes all BEM entities immutable.
132///
133@function iro-bem-validator--immutable-entities($type, $args, $selector, $context) {
134 @if not global-variable-exists(iro-bem-generated-selectors) {
135 $iro-bem-generated-selectors: () !global;
136 }
137
138 $block-name: null;
139 $block-type: null;
140 $block-id: null;
141
142 @if $type == 'block' {
143 $block-name: map-get($args, 'name');
144 $block-type: map-get($args, 'type');
145 } @else {
146 $block-context: iro-context-get($iro-bem-context-id, 'block');
147 $block-name: map-get(nth($block-context, 2), 'name');
148 $block-type: map-get(nth($block-context, 2), 'type');
149 }
150
151 @if $block-type != null {
152 $block-id: $block-name + '_' + $block-type;
153 } @else {
154 $block-id: $block-name;
155 }
156
157 @if $type == 'block' {
158 @if map-has-key($iro-bem-generated-selectors, $block-id) {
159 @return false 'Entity "#{$type}" with arguments [ #{iro-map-print($args)} ] was already defined.';
160 }
161
162 $iro-bem-generated-selectors: map-merge($iro-bem-generated-selectors, ($block-id: ())) !global;
163 } @else {
164 $selectors: map-get($iro-bem-generated-selectors, $block-id);
165
166 @if index($selectors, $selector) {
167 @return false 'Entity "#{$type}" with arguments [ #{iro-map-print($args)} ] was already defined.';
168 }
169
170 $selectors: append($selectors, $selector);
171
172 $iro-bem-generated-selectors: map-merge($iro-bem-generated-selectors, ($block-id: $selectors)) !global;
173 }
174
175 @return true '';
176}
diff --git a/src/bem/_vars.scss b/src/bem/_vars.scss
new file mode 100644
index 0000000..5942d4f
--- /dev/null
+++ b/src/bem/_vars.scss
@@ -0,0 +1,108 @@
1////
2/// @group BEM
3///
4/// @access public
5////
6
7///
8/// Separating character sequence for elements.
9///
10/// @type string
11///
12$iro-bem-element-separator: '__' !default;
13
14///
15/// Separating character sequence for modifiers.
16///
17/// @type string
18///
19$iro-bem-modifier-separator: '--' !default;
20
21///
22/// Separating character sequence for BEMIT suffixes.
23///
24/// @type string
25///
26$iro-bem-suffix-separator: '\\@' !default;
27
28///
29/// Prefixes for all BEMIT namespaces.
30///
31/// @prop {string} utility ['u'] - Utility namespace
32/// @prop {string} object ['o'] - Object namespace
33/// @prop {string} component ['c'] - Component namespace
34/// @prop {string} layout ['l'] - Layout namespace
35/// @prop {string} scope ['s'] - Scope namespace
36/// @prop {string} theme ['t'] - Theme namespace
37/// @prop {string} js ['js'] - JS namespace
38/// @prop {string} qa ['qa'] - QA namespace
39/// @prop {string} hack ['_'] - Hack namespace
40///
41/// @type map
42///
43$iro-bem-namespaces: (
44 object: 'o',
45 component: 'c',
46 layout: 'l',
47 scope: 's',
48 theme: 't',
49 utility: 'u',
50 js: 'js',
51 qa: 'qa',
52 hack: '_'
53) !default;
54
55///
56/// A list of all generated blocks.
57///
58/// @type list
59///
60/// @access private
61///
62$iro-bem-blocks: ();
63
64///
65/// Maximum nesting depth of BEM mixins. The large default value means there is no effective limit.
66///
67/// @type number
68///
69$iro-bem-max-depth: 99 !default;
70
71///
72/// Indicates how nested elements should be handled.
73///
74/// 'allow' means elements will be nested, i.e. the result will be {e} {b}__element.
75/// 'disallow' means an error will be emitted.
76/// 'append' means the element name will be appended to the parent element, i.e. the result will be {e}__element.
77/// Any other value is treated as 'allow'.
78///
79/// @type string
80///
81$iro-bem-element-nesting-policy: 'allow' !default;
82
83///
84/// Context ID used for all BEM-related mixins.
85///
86/// @type string
87///
88$iro-bem-context-id: 'bem' !default;
89
90///
91/// Debug mode.
92///
93/// @type bool
94///
95$iro-bem-debug: false !default;
96
97///
98/// Colors assigned to namespaces.
99///
100/// @type map
101///
102$iro-bem-debug-colors: (
103 object: #ffa500,
104 component: #00f,
105 layout: #ff0,
106 utility: #008000,
107 hack: #f00
108) !default;