From d07f664450ddaaebb44127a4bd057763d13d3f82 Mon Sep 17 00:00:00 2001 From: Feuerfuchs Date: Sun, 1 Nov 2020 20:55:14 +0100 Subject: Init --- src/bem/_block.scss | 392 +++++++++++++++++++++++++++++ src/bem/_debug.scss | 16 ++ src/bem/_element.scss | 622 +++++++++++++++++++++++++++++++++++++++++++++++ src/bem/_functions.scss | 26 ++ src/bem/_modifier.scss | 246 +++++++++++++++++++ src/bem/_multi.scss | 131 ++++++++++ src/bem/_state.scss | 146 +++++++++++ src/bem/_suffix.scss | 118 +++++++++ src/bem/_theme.scss | 61 +++++ src/bem/_validators.scss | 176 ++++++++++++++ src/bem/_vars.scss | 108 ++++++++ 11 files changed, 2042 insertions(+) create mode 100644 src/bem/_block.scss create mode 100644 src/bem/_debug.scss create mode 100644 src/bem/_element.scss create mode 100644 src/bem/_functions.scss create mode 100644 src/bem/_modifier.scss create mode 100644 src/bem/_multi.scss create mode 100644 src/bem/_state.scss create mode 100644 src/bem/_suffix.scss create mode 100644 src/bem/_theme.scss create mode 100644 src/bem/_validators.scss create mode 100644 src/bem/_vars.scss (limited to 'src/bem') 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 @@ +//// +/// @group BEM +/// +/// @access public +//// + +/// +/// Generate a new block. +/// +/// This mixin simply creates a new block with the name {namespace}_{name}, +/// where {namespace} is the prefix assigned to $type and {name} is the +/// block's name. +/// +/// @param {string} $name - Block name +/// @param {string} $type [null] - BEMIT namespace of the block +/// +/// @content +/// +/// @throw If $type is invalid +/// @throw If the block is preceded by another block, element, modifier or suffix +/// +/// @example scss - Creating a new block +/// @include iro-bem-block('something', 'component') { +/// /* some definitions */ +/// } +/// +/// // Generates: +/// +/// .c-something { +/// /* some definitions */ +/// } +/// +@mixin iro-bem-block($name, $type: null) { + $result: iro-bem-block($name, $type); + $selector: nth($result, 1); + $context: nth($result, 2); + + @include iro-bem-validate( + 'block', + (name: $name, type: $type), + $selector, + $context + ); + + @if $type != null { + $iro-bem-blocks: append($iro-bem-blocks, $name + '_' + $type) !global; + } @else { + $iro-bem-blocks: append($iro-bem-blocks, $name) !global; + } + + @include iro-context-push($iro-bem-context-id, $context...); + @at-root #{$selector} { + @content; + } + @include iro-context-pop($iro-bem-context-id); +} + +/// +/// Generate a new block. Check the respective mixin documentation for more information. +/// +/// @return {list} A list with two items: 1. selector, 2. context or `null` +/// +/// @see {mixin} iro-bem-block +/// +@function iro-bem-block($name, $type: null) { + // + // Possible outcomes: + // - ({b,e,m,s}) block + // + + $noop: iro-context-assert-stack-count($iro-bem-context-id, $iro-bem-max-depth); + + $selector: null; + $base-selector: null; + + @if $type != null { + $namespace: map-get($iro-bem-namespaces, $type); + + @if not $namespace { + @error '"#{$type}" is not a valid type.'; + } + + $base-selector: selector-parse('.' + $namespace + '-' + $name); + + @if $type != 'theme' or & { + $selector: $base-selector; + } @else if not & { + $selector: iro-bem-theme-selector($name); + } + } @else { + $base-selector: selector-parse('.' + $name); + $selector: $base-selector; + } + + @if & { + $selector: selector-nest(&, $selector); + } + + $context: 'block', ( + 'name': $name, + 'type': $type, + 'selector': $selector, + 'base-selector': $base-selector + ); + + @return $selector $context; +} + +/// +/// Generate a new object block. It's a shorthand for iro-bem-block($name, 'object'). +/// +/// @param {string} $name - Object block name +/// +/// @content +/// +@mixin iro-bem-object($name) { + @include iro-bem-block($name, 'object') { + @content; + } +} + +/// +/// Generate a new object block. Check the respective mixin documentation for more information. +/// +/// @return {list} A list with two items: 1. selector, 2. context or `null` +/// +/// @see {mixin} iro-bem-object +/// +@function iro-bem-object($name) { + @return iro-bem-block($name, 'object'); +} + +/// +/// Generate a new component block. It's a shorthand for iro-bem-block($name, 'component'). +/// +/// @param {string} $name - Component block name +/// +/// @content +/// +@mixin iro-bem-component($name) { + @include iro-bem-block($name, 'component') { + @content; + } +} + +/// +/// Generate a new component block. Check the respective mixin documentation for more information. +/// +/// @return {list} A list with two items: 1. selector, 2. context or `null` +/// +/// @see {mixin} iro-bem-component +/// +@function iro-bem-component($name) { + @return iro-bem-block($name, 'component'); +} + +/// +/// Generate a new layout block. It's a shorthand for iro-bem-block($name, 'layout'). +/// +/// @param {string} $name - Layout block name +/// +/// @content +/// +@mixin iro-bem-layout($name) { + @include iro-bem-block($name, 'layout') { + @content; + } +} + +/// +/// Generate a new layout block. Check the respective mixin documentation for more information. +/// +/// @return {list} A list with two items: 1. selector, 2. context or `null` +/// +/// @see {mixin} iro-bem-layout +/// +@function iro-bem-layout($name) { + @return iro-bem-block($name, 'layout'); +} + +/// +/// Generate a new utility block. It's a shorthand for iro-bem-block($name, 'utility'). +/// +/// @param {string} $name - Utility block name +/// +/// @content +/// +@mixin iro-bem-utility($name) { + @include iro-bem-block($name, 'utility') { + @content; + } +} + +/// +/// Generate a new utility block. Check the respective mixin documentation for more information. +/// +/// @return {list} A list with two items: 1. selector, 2. context or `null` +/// +/// @see {mixin} iro-bem-utility +/// +@function iro-bem-utility($name) { + @return iro-bem-block($name, 'utility'); +} + +/// +/// Generate a new scope block. It's a shorthand for iro-bem-block($name, 'scope'). +/// +/// @param {string} $name - Scope block name +/// +/// @content +/// +@mixin iro-bem-scope($name) { + @include iro-bem-block($name, 'scope') { + @content; + } +} + +/// +/// Generate a new scope block. Check the respective mixin documentation for more information. +/// +/// @return {list} A list with two items: 1. selector, 2. context or `null` +/// +/// @see {mixin} iro-bem-scope +/// +@function iro-bem-scope($name) { + @return iro-bem-block($name, 'scope'); +} + +/// +/// Generate a new theme block. It's a shorthand for iro-bem-block($name, 'theme'). +/// +/// @param {string} $name - Theme block name +/// +/// @content +/// +@mixin iro-bem-theme($name) { + @include iro-bem-block($name, 'theme') { + @content; + } +} + +/// +/// Generate a new theme block. Check the respective mixin documentation for more information. +/// +/// @return {list} A list with two items: 1. selector, 2. context or `null` +/// +/// @see {mixin} iro-bem-theme +/// +@function iro-bem-theme($name) { + @return iro-bem-block($name, 'theme'); +} + +/// +/// Generate a new JS block. It's a shorthand for iro-bem-block($name, 'js'). +/// +/// @param {string} $name - JS block name +/// +/// @content +/// +@mixin iro-bem-js($name) { + @include iro-bem-block($name, 'js') { + @content; + } +} + +/// +/// Generate a new JS block. Check the respective mixin documentation for more information. +/// +/// @return {list} A list with two items: 1. selector, 2. context or `null` +/// +/// @see {mixin} iro-bem-js +/// +@function iro-bem-js($name) { + @return iro-bem-block($name, 'js'); +} + +/// +/// Generate a new QA block. It's a shorthand for iro-bem-block($name, 'qa'). +/// +/// @param {string} $name - QA block name +/// +/// @content +/// +@mixin iro-bem-qa($name) { + @include iro-bem-block($name, 'qa') { + @content; + } +} + +/// +/// Generate a new QA block. Check the respective mixin documentation for more information. +/// +/// @return {list} A list with two items: 1. selector, 2. context or `null` +/// +/// @see {mixin} iro-bem-qa +/// +@function iro-bem-qa($name) { + @return iro-bem-block($name, 'qa'); +} + +/// +/// Generate a new hack block. It's a shorthand for iro-bem-block($name, 'hack'). +/// +/// @param {string} $name - Hack block name +/// +/// @content +/// +@mixin iro-bem-hack($name) { + @include iro-bem-block($name, 'hack') { + @content; + } +} + +/// +/// Generate a new hack block. Check the respective mixin documentation for more information. +/// +/// @return {list} A list with two items: 1. selector, 2. context or `null` +/// +/// @see {mixin} iro-bem-hack +/// +@function iro-bem-hack($name) { + @return iro-bem-block($name, 'hack'); +} + +/// +/// Assert that a block or element is composed of another block. In BEM, such a relationship is referred to +/// as a "mix": https://en.bem.info/methodology/key-concepts/#mix +/// +/// Compilation will fail if the foreign block doesn't exist. This way, you can ensure that blocks are +/// defined in the right order so that composed blocks/elements will actually override the foreign +/// declarations without having to artificially increase the specificity. +/// +/// @param {string | list} $block - Either first block name, or list with two items: 1. block name, 2. block type +/// @param {string | list} $blocks - Either other block names, or list with two items: 1. block name, 2. block type +/// +/// @throw If a block type is invalid +/// @throw If a block doesn't exist +/// +/// @example scss - Successful assertion +/// @include iro-bem-component('someBlock') { +/// /* some definitions */ +/// } +/// +/// @include iro-bem-component('anotherBlock') { +/// /* some definitions */ +/// +/// @include iro-bem-element('elem') { +/// @include iro-bem-composed-of('someBlock' 'component'); +/// +/// /* some definitions */ +/// } +/// } +/// +/// // Intended use:
...
+/// +/// @example scss - Failing assertion +/// @include iro-bem-component('anotherBlock') { +/// /* some definitions */ +/// +/// @include iro-bem-element('elem') { +/// @include iro-bem-composed-of('someBlock' 'component'); +/// +/// /* some definitions */ +/// } +/// } +/// +/// @include iro-bem-component('someBlock') { +/// /* some definitions */ +/// } +/// +/// // Compilation will fail because c-someBlock is defined after c-anotherBlock__elem +/// +@mixin iro-bem-composed-of($block, $blocks...) { + @each $block in iro-list-prepend($blocks, $block) { + @if type-of($block) == string { + @if not index($iro-bem-blocks, $block) { + @error 'Block "#{$block}" does not exist.'; + } + } @else { + $name: nth($block, 1); + $type: nth($block, 2); + + @if not map-get($iro-bem-namespaces, $type) { + @error '"#{$type}" is not a valid type.'; + } + + @if not index($iro-bem-blocks, $name + '_' + $type) { + @error 'Block "#{$name}" does not exist.'; + } + } + } +} 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 @@ +//// +/// @group BEM +/// +/// @access public +//// + +@if $iro-bem-debug { + @each $type, $color in $iro-bem-debug-colors { + $namespace: map-get($iro-bem-namespaces, $type); + + [class^='#{$namespace}-'], + [class*=' #{$namespace}-'] { + outline: 5px solid $color; + } + } +} 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 @@ +//// +/// @group BEM +/// +/// @access public +//// + +/// +/// Generate a new BEM element. +/// +/// The element will be generated according to the BEM naming convention. +/// If the parent selector doesn't match the block selector, the element will be +/// nested inside the parent selector. This means, you may nest elements inside +/// other elements, modifiers or any kind of selector such as &:hover. +/// +/// @param {string} $name - First element name +/// @param {string} $names - More element names +/// +/// @content +/// +/// @throw If the element is not preceded by a block, element, modifier or suffix. +/// +/// @example scss - Element for a block +/// @include iro-bem-component('block') { +/// /* some block definitions */ +/// +/// @include iro-bem-element('elem') { +/// /* some element definitions */ +/// } +/// } +/// +/// // Generates: +/// +/// .c-block { +/// /* some block definitions */ +/// } +/// +/// .c-block__elem { +/// /* some element definitions */ +/// } +/// +/// @example scss - Element that is affected by the user hovering the block +/// @include iro-bem-component('block') { +/// /* some block definitions */ +/// +/// @include iro-bem-element('elem') { +/// background-color: #eee; +/// } +/// +/// &:hover { +/// @include iro-bem-element('elem') { +/// background-color: #000; +/// } +/// } +/// } +/// +/// // Generates: +/// +/// .c-block { +/// /* some block definitions */ +/// } +/// +/// .c-block__elem { +/// background-color: #eee; +/// } +/// +/// .c-block:hover .c-block__elem { +/// background-color: #000; +/// } +/// +/// @example scss - Multiple elements +/// @include iro-bem-component('block') { +/// /* some block definitions */ +/// +/// @include iro-bem-element('elem1', 'elem2') { +/// /* some element definitions */ +/// } +/// } +/// +/// // Generates: +/// +/// .c-block { +/// /* some block definitions */ +/// } +/// +/// .c-block__elem1, .c-block__elem2 { +/// /* some element definitions */ +/// } +/// +@mixin iro-bem-element($name, $names...) { + $result: iro-bem-element($name, $names...); + $selector: nth($result, 1); + $context: nth($result, 2); + + @include iro-bem-validate( + 'element', + (name: $name, names: $names), + $selector, + $context + ); + + @include iro-context-push($iro-bem-context-id, $context...); + @at-root #{$selector} { + @content; + } + @include iro-context-pop($iro-bem-context-id); +} + +/// +/// Generate a new BEM element. Check the respective mixin documentation for more information. +/// +/// @return {list} A list with two items: 1. selector, 2. context or `null` +/// +/// @see {mixin} iro-bem-element +/// +@function iro-bem-element($name, $names...) { + $noop: iro-context-assert-stack-count($iro-bem-context-id, $iro-bem-max-depth); + $noop: iro-context-assert-stack-must-contain($iro-bem-context-id, 'block'); + + $parent-context: iro-context-get($iro-bem-context-id, 'block' 'element'); + + $selector: (); + $parts-data: (); + + @if nth($parent-context, 1) == 'element' { + @if $iro-bem-element-nesting-policy == 'disallow' { + @error 'Element nesting is forbidden.'; + } + + @if $iro-bem-element-nesting-policy == 'append' { + $element-selector: map-get(nth($parent-context, 2), 'selector'); + + @if not iro-selector-suffix-match(&, $element-selector) { + @error 'A nested element must be an immediate children of the parent element.'; + } + + // + // Possible outcomes: + // - {e}__element + // - [manual selector] {e}__element + // + + @each $name in join($name, $names) { + $sel: selector-append(&, $iro-bem-element-separator + $name); + $selector: join($selector, $sel, comma); + $parts-data: append($parts-data, ( + 'name': $name, + 'selector': $sel + )); + } + } + + $parent-context: iro-context-get($iro-bem-context-id, 'block'); + } + + @if length($selector) == 0 { + $parent-selector: map-get(nth($parent-context, 2), 'selector'); + + @if iro-selector-suffix-match(&, $parent-selector) { + // + // Possible outcomes: + // - {b}__element + // - [manual selector] {b}__element + // + + @each $name in join($name, $names) { + $sel: selector-append(&, $iro-bem-element-separator + $name); + $selector: join($selector, $sel, comma); + $parts-data: append($parts-data, ( + 'name': $name, + 'selector': $sel + )); + } + } @else { + // + // Possible outcomes: + // - {b} [manual selector] {b}__element + // - {e,m,s} ([manual selector]) {b}__element + // + + @if nth($parent-context, 1) != 'block' { + $parent-context: iro-context-get($iro-bem-context-id, 'block'); + } + + $block-base-selector: map-get(nth($parent-context, 2), 'base-selector'); + + @each $name in join($name, $names) { + $sel: selector-nest(&, selector-append($block-base-selector, $iro-bem-element-separator + $name)); + $selector: join($selector, $sel, comma); + $parts-data: append($parts-data, ( + 'name': $name, + 'selector': $sel + )); + } + } + } + + $context: 'element', ( + 'parts': $parts-data, + 'selector': $selector + ); + + @return $selector $context; +} + +/// +/// Generate a BEM element that is related to the current element. +/// +/// The generated element selector is appended to the current element selector. The $sign +/// determines the relationship. +/// +/// @param {string} $sign - Relationshop sign, either '+' or '~' +/// @param {string} $name - First element name +/// @param {string} $names - More element names +/// +/// @content +/// +/// @throw If the element is not preceded by an element. +/// +/// @example scss - A sibling element to a single element +/// @include iro-bem-component('block') { +/// @include iro-bem-element('elem') { +/// /* some element definitions */ +/// +/// @include iro-bem-related-element('~', 'sibling') { +/// /* some sibling element definitions */ +/// } +/// } +/// } +/// +/// // Generates: +/// +/// .c-block__elem { +/// /* some element definitions */ +/// } +/// +/// .c-block__elem ~ .c-block__sibling { +/// /* some sibling element definitions */ +/// } +/// +/// @example scss - A successor element to a single element +/// @include iro-bem-component('block') { +/// @include iro-bem-element('elem') { +/// /* some element definitions */ +/// +/// @include iro-bem-related-element('+', 'successor') { +/// /* some successor element definitions */ +/// } +/// } +/// } +/// +/// // Generates: +/// +/// .c-block__elem { +/// /* some element definitions */ +/// } +/// +/// .c-block__elem + .c-block__successor { +/// /* some successor element definitions */ +/// } +/// +/// @example scss - A successor element to multiple elements +/// @include iro-bem-component('block') { +/// @include iro-bem-element('elem1', 'elem2') { +/// /* some element definitions */ +/// +/// @include iro-bem-related-element('+', 'successor') { +/// /* some successor element definitions */ +/// } +/// } +/// } +/// +/// // Generates: +/// +/// .c-block__elem1, .c-block__elem2 { +/// /* some element definitions */ +/// } +/// +/// .c-block__elem1 + .c-block__successor, .c-block__elem2 + .c-block__successor { +/// /* some successor element definitions */ +/// } +/// +@mixin iro-bem-related-element($sign, $name, $names...) { + $result: iro-bem-related-element($sign, $name, $names...); + $selector: nth($result, 1); + $context: nth($result, 2); + + @include iro-bem-validate( + 'related-element', + (sign: $sign, name: $name, names: $names), + $selector, + $context + ); + + @include iro-context-push($iro-bem-context-id, $context...); + @at-root #{$selector} { + @content; + } + @include iro-context-pop($iro-bem-context-id); +} + +/// +/// Generate a new BEM element that is related to the current element. +/// Check the respective mixin documentation for more information. +/// +/// @return {list} A list with two items: 1. selector, 2. context or `null` +/// +/// @see {mixin} iro-bem-related-element +/// +@function iro-bem-related-element($sign, $name, $names...) { + // + // Generating this selector is simple: Take the latest block context, use it + // to generate the element part, and insert it at the end of the current selector. + // Possible outcomes: + // - {e} ({m,s}) ([manual selector]) + {e} + // - {e} ({m,s}) ([manual selector]) ~ {e} + // + + $noop: iro-context-assert-stack-count($iro-bem-context-id, $iro-bem-max-depth); + $noop: iro-context-assert-stack-must-contain($iro-bem-context-id, 'element'); + + @if $sign != '+' and $sign != '~' { + @error 'Invalid relationship sign #{inspect($sign)}.'; + } + + $block-context: iro-context-get($iro-bem-context-id, 'block'); + $block-base-selector: map-get(nth($block-context, 2), 'base-selector'); + + $selector: (); + $parts-data: (); + + @each $name in join($name, $names) { + $sel: selector-nest(&, $sign, selector-append($block-base-selector, $iro-bem-element-separator + $name)); + $selector: join($selector, $sel, comma); + $parts-data: append($parts-data, ( + 'name': $name, + 'selector': $sel + )); + } + + $context: 'element', ( + 'parts': $parts-data, + 'selector': $selector + ); + + @return $selector $context; +} + +/// +/// Generate a BEM element that is a sibling of the current element. +/// +/// It's a shorthand for iro-bem-related-element('~', $name). +/// +/// @param {string} $name - First element name +/// @param {list} $names - List of more element names +/// +/// @content +/// +@mixin iro-bem-sibling-element($name, $names...) { + @include iro-bem-related-element('~', $name, $names...) { + @content; + } +} + +/// +/// Generate a new BEM element that is a sibling of the current element. +/// Check the respective mixin documentation for more information. +/// +/// @return {list} A list with two items: 1. selector, 2. context or `null` +/// +/// @see {mixin} iro-bem-sibling-element +/// +@function iro-bem-sibling-element($name, $names...) { + @return iro-bem-related-element('~', $name, $names...); +} + +/// +/// Generate a BEM element that is the successor of the current element. +/// +/// It's a shorthand for iro-bem-related-element('+', $name). +/// +/// @param {string} $name - First element name +/// @param {string} $names - More element names +/// +/// @content +/// +@mixin iro-bem-next-element($name, $names...) { + @include iro-bem-related-element('+', $name, $names...) { + @content; + } +} + +/// +/// Generate a new BEM element that is the successor of the current element. +/// Check the respective mixin documentation for more information. +/// +/// @return {list} A list with two items: 1. selector, 2. context or `null` +/// +/// @see {mixin} iro-bem-next-element +/// +@function iro-bem-next-element($name, $names...) { + @return iro-bem-related-element('+', $name, $names...); +} + +/// +/// Generate the current BEM element as a successor of itself. +/// +/// If this is applied to a single element, it behaves exactly the same as +/// iro-bem-related-element('+', name); +/// However, if it is applied to multiple elements, each twin element only will influence +/// their other twin, which is not replicable with iro-bem-related-element. +/// +/// @content +/// +/// @example scss - Two twin elements +/// @include iro-bem-component('block') { +/// @include iro-bem-element('elem') { +/// /* some element definitions */ +/// +/// @include iro-bem-next-twin-element { +/// /* some twin element definitions */ +/// } +/// } +/// } +/// +/// // Generates: +/// +/// .c-block__elem { +/// /* some element definitions */ +/// } +/// +/// .c-block__elem + .c-block__elem { +/// /* some twin element definitions */ +/// } +/// +/// @example scss - Multiple twin elements +/// @include iro-bem-component('block') { +/// @include iro-bem-element('elem1', 'elem2') { +/// /* some element definitions */ +/// +/// @include iro-bem-next-twin-element { +/// /* some twin element definitions */ +/// } +/// } +/// } +/// +/// // Generates: +/// +/// .c-block__elem1, .c-block__elem2 { +/// /* some element definitions */ +/// } +/// +/// .c-block__elem1 + .c-block__elem1, .c-block__elem2 + .c-block__elem2 { +/// /* some twin element definitions */ +/// } +/// +@mixin iro-bem-related-twin-element($sign) { + $result: iro-bem-related-twin-element($sign); + $selector: nth($result, 1); + $context: nth($result, 2); + + @include iro-bem-validate( + 'next-twin-element', + (), + $selector, + $context + ); + + @include iro-context-push($iro-bem-context-id, $context...); + @at-root #{$selector} { + @content; + } + @include iro-context-pop($iro-bem-context-id); +} + +/// +/// Generate the current BEM element as a successor of itself. +/// Check the respective mixin documentation for more information. +/// +/// @return {list} A list with two items: 1. selector, 2. context or `null` +/// +/// @see {mixin} iro-bem-next-twin-element +/// +@function iro-bem-related-twin-element($sign) { + $noop: iro-context-assert-stack-count($iro-bem-context-id, $iro-bem-max-depth); + $noop: iro-context-assert-stack-must-contain($iro-bem-context-id, 'element'); + + $element-context: iro-context-get($iro-bem-context-id, 'element'); + $element-selector: map-get(nth($element-context, 2), 'selector'); + + $block-context: iro-context-get($iro-bem-context-id, 'block'); + $block-base-selector: map-get(nth($block-context, 2), 'base-selector'); + + $selector: (); + $parts-data: (); + + // + // To determine the twin for each element, iterate the sub-selectors from the current selector + // and check if it contains the currently inspected element. This has to be done with string + // comparison since none of Sass selector functions is of use here. + // Finally, the current twin will be appended to the extracted sub-selector as a successor + // element. + // + @each $part-data in map-get(nth($element-context, 2), 'parts') { + $part-selector: map-get($part-data, 'selector'); + $part-name: map-get($part-data, 'name'); + + $sel: (); + @if iro-selector-suffix-match(&, $element-selector) { + // + // This mixin is included in the selector the last element mixin created. + // Possible outcomes: + // - {e} + {e} + // - [manual selector] {e} + {e} + // + + @each $s in & { + @each $ps in $part-selector { + @if nth($s, -1) == nth($ps, -1) { + $sel-ent: selector-nest($s, $sign, selector-append($block-base-selector, $iro-bem-element-separator + $part-name)); + $sel: join($sel, $sel-ent, comma); + } + } + } + } @else { + // + // This mixin is NOT included in the selector the last element mixin created. + // Possible outcomes: + // - {e} {m,s} + {e} + // - {e} [manual selector] + {e} + // - {e} {m,s} [manual selector] + {e} + // + + @each $s in & { + @each $ps in $part-selector { + @if str-index(inspect($s), inspect($ps)) { + $char-index: str-length(inspect($ps)) + 1; + $match: index(' ' ':' ',', str-slice(inspect($s), $char-index, $char-index)) != null; + + @if not $match { + @each $separator in $iro-bem-element-separator $iro-bem-modifier-separator $iro-bem-suffix-separator { + @if str-slice(inspect($s), $char-index, $char-index + str-length($separator) - 1) == $separator { + $match: true; + } + } + } + + @if $match { + $sel-ent: selector-nest($s, '+', selector-append($block-base-selector, $iro-bem-element-separator + $part-name)); + $sel: join($sel, $sel-ent, comma); + } + } + } + } + } + @if length($sel) != length($part-selector) { + @error 'Could not generate twin element selector.'; + } + + $selector: join($selector, $sel, comma); + $parts-data: append($parts-data, ( + 'name': $part-name, + 'selector': $sel + )); + } + + $context: 'element', ( + 'parts': $parts-data, + 'selector': $selector + ); + + @return $selector $context; +} + +/// +/// Generate the current BEM element as a sibling of itself. +/// +/// It's a shorthand for iro-bem-related-twin-element('~'). +/// +/// @content +/// +@mixin iro-bem-sibling-twin-element { + @include iro-bem-related-twin-element('~') { + @content; + } +} + +/// +/// Generate the current BEM element as a sibling of itself. +/// Check the respective mixin documentation for more information. +/// +/// @return {list} A list with two items: 1. selector, 2. context or `null` +/// +/// @see {mixin} iro-bem-sibling-twin-element +/// +@function iro-bem-sibling-twin-element() { + @return iro-bem-related-twin-element('~'); +} + +/// +/// Generate the current BEM element as a next sibling of itself. +/// +/// It's a shorthand for iro-bem-related-twin-element('+', $name). +/// +/// @content +/// +@mixin iro-bem-next-twin-element { + @include iro-bem-related-twin-element('+') { + @content; + } +} + +/// +/// Generate the current BEM element as a next sibling of itself. +/// Check the respective mixin documentation for more information. +/// +/// @return {list} A list with two items: 1. selector, 2. context or `null` +/// +/// @see {mixin} iro-bem-next-twin-element +/// +@function iro-bem-next-twin-element() { + @return iro-bem-related-twin-element('+'); +} 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 @@ +//// +/// @group BEM +/// +/// @access public +//// + +/// +/// @access private +/// +@function iro-bem-theme-selector($name, $names...) { + $namespace: map-get($iro-bem-namespaces, 'theme'); + $selector: null; + + @each $name in join($name, $names) { + $sel: '.' + $namespace + '-' + $name; + + @if $selector == null { + $selector: join(selector-parse($sel), selector-parse('[class*=\' t-\'] ' + $sel), comma); + $selector: join($selector, selector-parse('[class^=\'t-\'] ' + $sel), comma); + } @else { + $selector: selector-nest($selector, $sel); + } + } + + @return $selector; +} 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 @@ +//// +/// @group BEM +/// +/// @access public +//// + +/// +/// Generate a new BEM modifier. +/// +/// If the parent context is block or element, the modifier will modify said block or element according +/// to the BEM naming convention. +/// +/// If the parent context is a modifier or suffix, then the modifier will depend on said modifier or suffix. +/// Depending on $extend, the meaning of this dependency (and the resulting selector) varies: +/// If it's false (default), you signalize that the modifier also exists by itself, but it changes its +/// behavior when the parent modifier or suffix is set. +/// If it's true, you signalize that the modifier extends the parent modifier or suffix and can only be +/// used in conjunction with it. +/// +/// @param {string | list} $name - First element name or list with two items: 1. first element name, 2. bool indicating if the modifier is extending +/// @param {string | list} $names - More element names or lists with two items: 1. element name, 2. bool indicating if the modifier is extending +/// +/// @content +/// +/// @throw If the element is not preceded by a block, element, modifier or suffix. +/// +/// @example scss - Modifier that modifies a block or element +/// @include iro-bem-component('block') { +/// @include iro-bem-modifier('mod') { +/// background-color: #eee; +/// } +/// +/// @include iro-bem-element('elem') { +/// @include iro-bem-modifier('mod') { +/// background-color: #222; +/// } +/// } +/// } +/// +/// // Generates: +/// +/// .c-block--mod { +/// background-color: #eee; +/// } +/// +/// .c-block__elem--mod { +/// background-color: #222; +/// } +/// +/// @example scss - Modifier nested in another modifier, not extending +/// @include iro-bem-component('block') { +/// @include iro-bem-modifier('mod') { +/// background-color: #eee; +/// } +/// +/// @include iro-bem-modifier('dark') { +/// /* some definitions */ +/// +/// @include iro-bem-modifier('mod') { +/// background-color: #222; +/// } +/// } +/// } +/// +/// // Generates: +/// +/// .c-block--mod { +/// background-color: #eee; +/// } +/// +/// .c-block--dark { +/// /* some definitions */ +/// } +/// +/// .c-block--dark.c-block--mod { +/// background-color: #222; +/// } +/// +/// @example scss - Modifier nested in another modifier, extending +/// @include iro-bem-component('block') { +/// @include iro-bem-modifier('mod') { +/// background-color: #eee; +/// } +/// +/// @include iro-bem-modifier('dark') { +/// /* some definitions */ +/// +/// @include iro-bem-modifier('mod' true) { +/// background-color: #222; +/// } +/// } +/// } +/// +/// // Generates: +/// +/// .c-block--mod { +/// background-color: #eee; +/// } +/// +/// .c-block--dark { +/// /* some definitions */ +/// } +/// +/// .c-block--dark--mod { +/// background-color: #222; +/// } +/// +@mixin iro-bem-modifier($name, $names...) { + $result: iro-bem-modifier($name, $names...); + $selector: nth($result, 1); + $context: nth($result, 2); + + @include iro-bem-validate( + 'modifier', + (name: $name, names: $names), + $selector, + $context + ); + + @include iro-context-push($iro-bem-context-id, $context...); + @at-root #{$selector} { + @content; + } + @include iro-context-pop($iro-bem-context-id); +} + +/// +/// Generate a new BEM modifier. Check the respective mixin documentation for more information. +/// +/// @return {list} A list with two items: 1. selector, 2. context or `null` +/// +/// @see {mixin} iro-bem-modifier +/// +@function iro-bem-modifier($name, $names...) { + $noop: iro-context-assert-stack-count($iro-bem-context-id, $iro-bem-max-depth); + $noop: iro-context-assert-stack-must-contain($iro-bem-context-id, 'block'); + + $parent-context: iro-context-get($iro-bem-context-id, 'block' 'element' 'modifier' 'suffix'); + $parent-selector: map-get(nth($parent-context, 2), 'selector'); + $selector: (); + $parts-data: (); + + @if not iro-selector-suffix-match(&, $parent-selector) { + // + // The current selector doesn't match the parent selector. + // The user manually added a selector between parent context and this modifier call. + // This case is forbidden because any outcome semantically wouldn't make sense: + // - {b,e,m,s} [manual selector] {b,e,m,s}--modifier + // - {b,e,m,s}--modifier [manual selector] + // The first case would make the modifier behave like an element. + // The second case is unintuitive, the code would be more clear by nesting the manual + // selector in the modifier instead. + // + + @error 'A modifier must be an immediate child of the parent context'; + } + + @each $name in iro-list-prepend($names, $name) { + $extend: false; + @if type-of($name) == list { + $extend: nth($name, 2); + $name: nth($name, 1); + } + + @if index('block' 'element', nth($parent-context, 1)) or $extend == true { + // + // Either the parent context is block or element, or a modifier or suffix + // is to be extended. The modifier part can simply be appended. + // Possible outcomes: + // - {b,e,m,s}--modifier + // + + $sel: selector-append(&, $iro-bem-modifier-separator + $name); + $selector: join($selector, $sel, comma); + $parts-data: append($parts-data, ( + 'name': $name, + 'selector': $sel + )); + } @else { + // + // Parent context is modifier, suffix or state and $extend is false. + // + + $be-context: iro-context-get($iro-bem-context-id, 'block' 'element'); + + @if nth($be-context, 1) == 'element' { + // + // Latest context is element. Since element contexts can consist of multiple single + // elements, inspect all elements and append its selector with the suffix "--$name". + // This has to be done with string comparison since none of Sass selector functions + // is of use here. + // Possible outcomes: + // - {m,s}.{e}--modifier + // + + $nsel: (); + + @each $elem-part-data in map-get(nth($be-context, 2), 'parts') { + $elem-part-selector: map-get($elem-part-data, 'selector'); + + $sel: (); + @each $s in & { + @each $ps in $elem-part-selector { + @if str-index(inspect($s), inspect($ps) + $iro-bem-modifier-separator) or str-index(inspect($s), inspect($ps) + $iro-bem-suffix-separator) { + $sel: join($sel, selector-unify($s, selector-append($ps, $iro-bem-modifier-separator + $name)), comma); + } + } + } + @if length($sel) == 0 { + @error 'Could not generate modifier selector.'; + } + + $nsel: join($nsel, $sel, comma); + } + + $selector: join($selector, $nsel, comma); + $parts-data: append($parts-data, ( + 'name': $name, + 'selector': $nsel + )); + } @else { + // + // Latest context is block. Just append the modifier part. + // Possible outcomes: + // - {m,s}.{b}--modifier + // + + $block-base-selector: map-get(nth($be-context, 2), 'base-selector'); + + $sel: selector-append(&, $block-base-selector, $iro-bem-modifier-separator + $name); + $selector: join($selector, $sel, comma); + $parts-data: append($parts-data, ( + 'name': $name, + 'selector': $sel + )); + } + } + } + + $context: 'modifier', ( + 'parts': $parts-data, + 'selector': $selector + ); + + @return $selector $context; +} 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 @@ +//// +/// @group BEM +/// +/// @access public +//// + +/// +/// Generate multiple entities (BEM or not) at once. +/// +/// NOTE: This mixin does not generate perfectly optimized selectors in order to keep track of contexts. +/// +/// @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. +/// @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. +/// +/// @content +/// +/// @example scss - Creating multiple elements, a modifier and an anchor +/// @include iro-bem-object('buttonstrip') { +/// display: none; +/// +/// @include iro-bem-multi('modifier' 'mod', 'element' 'button' 'separator', '> a') { +/// display: block; +/// } +/// } +/// +/// // Generates: +/// +/// .o-buttonstrip { +/// display: none; +/// } +/// +/// .o-buttonstrip--mod { +/// display: block; +/// } +/// +/// .o-buttonstrip__button, { +/// .o-buttonstrip__separator { +/// display: block; +/// } +/// +/// .o-buttonstrip > a { +/// display: block; +/// } +/// +/// @example scss - Creating multiple elements, a modifier and an anchor - optional colons included +/// @include iro-bem-object('buttonstrip') { +/// display: none; +/// +/// @include iro-bem-multi('modifier:' 'mod', 'element:' 'button' 'separator', '> a') { +/// display: block; +/// } +/// } +/// +/// // Generates: +/// +/// .o-buttonstrip { +/// display: none; +/// } +/// +/// .o-buttonstrip--mod { +/// display: block; +/// } +/// +/// .o-buttonstrip__button, { +/// .o-buttonstrip__separator { +/// display: block; +/// } +/// +/// .o-buttonstrip > a { +/// display: block; +/// } +/// +@mixin iro-bem-multi($first, $others...) { + @include iro-context-assert-stack-count($iro-bem-context-id, $iro-bem-max-depth); + + @each $entity in iro-list-prepend($others, $first) { + $is-manual-selector: false; + + @if type-of($entity) == string and not function-exists('iro-bem-' + $entity) { + $is-manual-selector: true; + } + + @if $is-manual-selector { + $sel: if(&, selector-nest(&, $entity), selector-parse($entity)); + + @at-root #{$sel} { + @content; + } + } @else { + $entity-func-id: null; + + @if type-of($entity) == list { + $entity-func-id: nth($entity, 1); + $entity: iro-list-slice($entity, 2); + } @else { + $entity-func-id: $entity; + $entity: (); + } + + @if str-slice($entity-func-id, str-length($entity-func-id)) == ':' { + $entity-func-id: str-slice($entity-func-id, 1, str-length($entity-func-id) - 1); + } + + $sel-func: null; + + @if function-exists('iro-bem-' + $entity-func-id) { + $sel-func: get-function('iro-bem-' + $entity-func-id); + } @else if function-exists($entity-func-id) { + $sel-func: get-function($entity-func-id); + } + + @if $sel-func == null { + @error 'Function "#{inspect($entity-func-id)}" was not found.'; + } + + $entity-result: call($sel-func, $entity...); + $entity-result-selector: nth($entity-result, 1); + $entity-result-context: nth($entity-result, 2); + + @if $entity-result-context != null { + @include iro-context-push($iro-bem-context-id, $entity-result-context...); + } + @at-root #{$entity-result-selector} { + @content; + } + @if $entity-result-context != null { + @include iro-context-pop($iro-bem-context-id); + } + } + } +} 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 @@ +//// +/// @group BEM +/// +/// @access public +//// + +/// +/// Create a new state rule. +/// +/// @param {string} $state - First state name +/// @param {list} $states - List of more state names +/// +/// @content +/// +/// @example scss - Using single is-state +/// @include iro-bem-object('menu') { +/// display: none; +/// +/// @include iro-bem-state('is', open') { +/// display: block; +/// } +/// } +/// +/// // Generates: +/// +/// .o-menu { +/// display: none; +/// } +/// +/// .o-menu.is-open { +/// display: block; +/// } +/// +/// @example scss - Using multiple is-states +/// @include iro-bem-object('menu') { +/// display: none; +/// +/// @include iro-bem-state('is', open', 'visible') { +/// display: block; +/// } +/// } +/// +/// // Generates: +/// +/// .o-menu { +/// display: none; +/// } +/// +/// .o-menu.is-open, +/// .o-menu.is-visible { +/// display: block; +/// } +/// +@mixin iro-bem-state($prefix, $state, $states...) { + $result: iro-bem-state($prefix, $state, $states...); + $selector: nth($result, 1); + $context: nth($result, 2); + + @include iro-bem-validate( + 'state', + (prefix: $prefix, state: $state, states: $states), + $selector, + $context + ); + + @include iro-context-push($iro-bem-context-id, $context...); + @at-root #{$selector} { + @content; + } + @include iro-context-pop($iro-bem-context-id); +} + +/// +/// Generate a new state. +/// +/// @return {list} A list with two items: 1. selector, 2. context or `null` +/// +/// @see {mixin} iro-bem-has +/// +@function iro-bem-state($prefix, $state, $states...) { + $selector: (); + $parts-data: (); + + @each $state in join($state, $states) { + $sel: selector-parse('.#{$prefix}-#{$state}'); + @if & { + $sel: selector-append(&, $sel); + } + $selector: join($selector, $sel, comma); + $parts-data: append($parts-data, ( + 'name': $state, + 'selector': $sel + )); + } + + $context: 'state', ( + 'parts': $parts-data, + 'selector': $selector + ); + + @return $selector $context; +} + +/// +/// Create a new has-state modifier. +/// +/// It's a shorthand for iro-bem-state('is', $state, $states...). +/// +@mixin iro-bem-is($state, $states...) { + @include iro-bem-state('is', $state, $states...) { + @content; + } +} + +/// +/// Generate a new is-state modifier. Check the respective mixin documentation for more information. +/// +/// @return {list} A list with two items: 1. selector, 2. context or `null` +/// +/// @see {mixin} iro-bem-is +/// +@function iro-bem-is($state, $states...) { + @return iro-bem-state('is', $state, $states...); +} + +/// +/// Create a new has-state modifier. +/// +/// It's a shorthand for iro-bem-state('has', $state, $states...). +/// +@mixin iro-bem-has($state, $states...) { + @include iro-bem-state('has', $state, $states...) { + @content; + } +} + +/// +/// Generate a new has-state modifier. Check the respective mixin documentation for more information. +/// +/// @return {list} A list with two items: 1. selector, 2. context or `null` +/// +/// @see {mixin} iro-bem-has +/// +@function iro-bem-has($state, $states...) { + @return iro-bem-state('has', $state, $states...); +} 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 @@ +//// +/// @group BEM +/// +/// @access public +//// + +/// +/// Generate a new suffix. +/// +/// @param {string} $name - Suffix name +/// +/// @content +/// +/// @throw If the element is not preceded by a block or modifier. +/// +/// @example scss - Using a suffix +/// @include iro-bem-utility('hidden') { +/// display: none; +/// +/// @media (max-width: 320px) { +/// @include iro-bem-suffix('phone') { +/// display: none; +/// } +/// } +/// +/// @media (max-width: 768px) { +/// @include iro-bem-suffix('tablet') { +/// display: none; +/// } +/// } +/// } +/// +/// // Generates: +/// +/// .u-hidden { +/// display: none; +/// } +/// +/// @media (max-width: 320px) { +/// .u-hidden@phone { +/// display: none; +/// } +/// } +/// +/// @media (max-width: 768px) { +/// .u-hidden@tablet { +/// display: none; +/// } +/// } +/// +@mixin iro-bem-suffix($name) { + $result: iro-bem-suffix($name); + $selector: nth($result, 1); + $context: nth($result, 2); + + @include iro-bem-validate( + 'suffix', + (name: $name), + $selector, + $context + ); + + @include iro-context-push($iro-bem-context-id, $context...); + @at-root #{$selector} { + @content; + } + @include iro-context-pop($iro-bem-context-id); +} + +/// +/// Generate a new suffix. Check the respective mixin documentation for more information. +/// +/// @return {list} A list with two items: 1. selector, 2. context or `null` +/// +/// @see {mixin} iro-bem-suffix +/// +@function iro-bem-suffix($name) { + // + // Suffixes can be used on block, element and modifier. + // + + $noop: iro-context-assert-stack-count($iro-bem-context-id, $iro-bem-max-depth); + $noop: iro-context-assert-stack-must-contain($iro-bem-context-id, 'block'); + $noop: iro-context-assert-stack-must-not-contain($iro-bem-context-id, 'suffix'); + + $parent-context: iro-context-get($iro-bem-context-id, 'block' 'element' 'modifier'); + $parent-selector: map-get(nth($parent-context, 2), 'selector'); + + @if not iro-selector-suffix-match(&, $parent-selector) { + // + // The current selector doesn't match the parent selector. + // The user manually added a selector between parent context and this suffix call. + // This case is forbidden because any outcome semantically wouldn't make sense: + // - {b,e,m} [manual selector] {b,e,m}@suffix + // - {b,e,m}@suffix [manual selector] + // The first case would make the modifier behave like an element. + // The second case is unintuitive, the code would be more clear by nesting the manual + // selector in the suffix instead. + // + + @error 'A suffix must be an immediate child of the parent context'; + } + + // + // The suffix part can simply be appended. + // Possible outcomes: + // - {b,e,m}@suffix + // + + $selector: selector-append(&, $iro-bem-suffix-separator + $name); + + $context: 'suffix', ( + 'name': $name, + 'selector': $selector + ); + + @return $selector $context; +} 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 @@ +//// +/// @group BEM +/// +/// @access public +//// + +/// +/// Declare new rules for the current block for when this theme is active. +/// +/// @param {string} $name - First theme block name +/// @param {string} $names - More theme block names +/// +/// @content +/// +@mixin iro-bem-at-theme($name, $names...) { + $result: iro-bem-at-theme($name, $names...); + $selector: nth($result, 1); + $context: nth($result, 2); + + @include iro-bem-validate( + 'at-theme', + (name: $name, names: $names), + $selector, + $context + ); + + @include iro-context-push($iro-bem-context-id, $context...); + @at-root #{$selector} { + @content; + } + @include iro-context-pop($iro-bem-context-id); +} + +/// +/// Declare new rules for the current block for when this theme is active. +/// Check the respective mixin documentation for more information. +/// +/// @return {list} A list with two items: 1. selector, 2. context or `null` +/// +/// @see {mixin} iro-bem-at-theme +/// +@function iro-bem-at-theme($name, $names...) { + $noop: iro-context-assert-stack-must-contain($iro-bem-context-id, 'block'); + + $parent-context: iro-context-get($iro-bem-context-id, 'block'); + $parent-selector: map-get(nth($parent-context, 2), 'selector'); + + @if not iro-selector-suffix-match(&, $parent-selector) { + @error 'An at-theme rule must be an immediate child of a block'; + } + + $selector: iro-bem-theme-selector($name, $names...); + $selector: selector-nest($selector, &); + + $context: 'at-theme', ( + 'name': join($name, $names), + 'selector': $selector + ); + + @return $selector $context; +} 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 @@ +//// +/// Validators are custom functions that will be called before a BEM entity is created. +/// They check if the current mixin usage is valid or not and thus they are a flexible way to +/// let you implement your own rules. +/// +/// Validator functions receive the following information: +/// - BEM entity type +/// - Arguments passed to the mixin +/// - The generated selector +/// - The generated context, if any +/// +/// Additionally, the context stack used by the BEM system can be examined. +/// +/// @group BEM +/// +/// @access public +//// + +/// +/// A list of validator functions. +/// +/// @type list +/// +/// @access private +/// +$iro-bem-validators: (); + +/// +/// Register one or multiple validator functions. +/// +/// A validator function is a function that accepts 4 arguments: +/// 1. BEM entity type (string) +/// 2. Arguments passed to the mixin (map) +/// 3. The generated selector (selector) +/// 4. The generated context (list, may be null) +/// +/// The function must return a list with two items: +/// 1. `true` if the mixin usage is valid, otherwise `false`, +/// 2. a string with a rejection reason (empty if the usage is valid). +/// +/// @param {string} $func-name - First function name. +/// @param {string} $func-names - Other function names. +/// +@mixin iro-bem-add-validator($func-name, $func-names...) { + $noop: iro-bem-add-validator($func-name, $func-names...); +} + +/// +/// Register one or multiple validator functions. Check the respective mixin documentation for more information. +/// +/// @see {mixin} iro-bem-add-validator +/// +@function iro-bem-add-validator($func-name, $func-names...) { + @each $fn-name in join($func-name, $func-names) { + $fn: get-function($fn-name); + $iro-bem-validators: map-merge($iro-bem-validators, ($fn-name: $fn)) !global; + } + @return null; +} + +/// +/// Unregister one or multiple validator functions. +/// +/// @param {string} $func-name - First function name. +/// @param {string} $func-names - Other function names. +/// +@mixin iro-bem-remove-validator($func-name, $func-names...) { + $noop: iro-bem-remove-validator($func-name, $func-names...); +} + +/// +/// Unregister one or multiple validator functions. Check the respective mixin documentation for more information. +/// +/// @see {mixin} iro-bem-remove-validator +/// +@function iro-bem-remove-validator($func-name, $func-names...) { + $iro-bem-validators: map-remove($iro-bem-validators, $func-name, $func-names...) !global; + @return null; +} + +/// +/// @access private +/// +@mixin iro-bem-validate($type, $args, $selector, $context) { + @each $id, $fn in $iro-bem-validators { + $result: call($fn, $type, $args, $selector, $context); + @if not nth($result, 1) { + @error 'A BEM validator rejected this mixin usage due to the following reason: #{nth($result, 2)}'; + } + } +} + +// +// --------------------------------------------------------------------------------------------------------- +// Built-in validators +// --------------------------------------------------------------------------------------------------------- +// + +/// +/// A validator that makes sure blocks are declared in the right order, determined by the +/// namespace used. +/// +@function iro-bem-validator--enforce-namespace-order($type, $args, $selector, $context) { + @if not global-variable-exists(iro-bem-namespace-order) { + $iro-bem-namespace-order: map-keys($iro-bem-namespaces) !global; + } + @if not global-variable-exists(iro-bem-cur-namespace-index) { + $iro-bem-cur-namespace-index: 1 !global; + } + + @if $type == 'block' { + $block-type: map-get($args, 'type'); + + @if $block-type != null { + $ns-index: index($iro-bem-namespace-order, $block-type); + + @if $ns-index != null { + @if $ns-index < $iro-bem-cur-namespace-index { + @return false 'Namespace "#{$block-type}" comes before current namespace #{nth($iro-bem-namespace-order, $iro-bem-cur-namespace-index)}'; + } + + $iro-bem-cur-namespace-index: $ns-index !global; + } + } + } + + @return true ''; +} + +/// +/// A validator that makes all BEM entities immutable. +/// +@function iro-bem-validator--immutable-entities($type, $args, $selector, $context) { + @if not global-variable-exists(iro-bem-generated-selectors) { + $iro-bem-generated-selectors: () !global; + } + + $block-name: null; + $block-type: null; + $block-id: null; + + @if $type == 'block' { + $block-name: map-get($args, 'name'); + $block-type: map-get($args, 'type'); + } @else { + $block-context: iro-context-get($iro-bem-context-id, 'block'); + $block-name: map-get(nth($block-context, 2), 'name'); + $block-type: map-get(nth($block-context, 2), 'type'); + } + + @if $block-type != null { + $block-id: $block-name + '_' + $block-type; + } @else { + $block-id: $block-name; + } + + @if $type == 'block' { + @if map-has-key($iro-bem-generated-selectors, $block-id) { + @return false 'Entity "#{$type}" with arguments [ #{iro-map-print($args)} ] was already defined.'; + } + + $iro-bem-generated-selectors: map-merge($iro-bem-generated-selectors, ($block-id: ())) !global; + } @else { + $selectors: map-get($iro-bem-generated-selectors, $block-id); + + @if index($selectors, $selector) { + @return false 'Entity "#{$type}" with arguments [ #{iro-map-print($args)} ] was already defined.'; + } + + $selectors: append($selectors, $selector); + + $iro-bem-generated-selectors: map-merge($iro-bem-generated-selectors, ($block-id: $selectors)) !global; + } + + @return true ''; +} 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 @@ +//// +/// @group BEM +/// +/// @access public +//// + +/// +/// Separating character sequence for elements. +/// +/// @type string +/// +$iro-bem-element-separator: '__' !default; + +/// +/// Separating character sequence for modifiers. +/// +/// @type string +/// +$iro-bem-modifier-separator: '--' !default; + +/// +/// Separating character sequence for BEMIT suffixes. +/// +/// @type string +/// +$iro-bem-suffix-separator: '\\@' !default; + +/// +/// Prefixes for all BEMIT namespaces. +/// +/// @prop {string} utility ['u'] - Utility namespace +/// @prop {string} object ['o'] - Object namespace +/// @prop {string} component ['c'] - Component namespace +/// @prop {string} layout ['l'] - Layout namespace +/// @prop {string} scope ['s'] - Scope namespace +/// @prop {string} theme ['t'] - Theme namespace +/// @prop {string} js ['js'] - JS namespace +/// @prop {string} qa ['qa'] - QA namespace +/// @prop {string} hack ['_'] - Hack namespace +/// +/// @type map +/// +$iro-bem-namespaces: ( + object: 'o', + component: 'c', + layout: 'l', + scope: 's', + theme: 't', + utility: 'u', + js: 'js', + qa: 'qa', + hack: '_' +) !default; + +/// +/// A list of all generated blocks. +/// +/// @type list +/// +/// @access private +/// +$iro-bem-blocks: (); + +/// +/// Maximum nesting depth of BEM mixins. The large default value means there is no effective limit. +/// +/// @type number +/// +$iro-bem-max-depth: 99 !default; + +/// +/// Indicates how nested elements should be handled. +/// +/// 'allow' means elements will be nested, i.e. the result will be {e} {b}__element. +/// 'disallow' means an error will be emitted. +/// 'append' means the element name will be appended to the parent element, i.e. the result will be {e}__element. +/// Any other value is treated as 'allow'. +/// +/// @type string +/// +$iro-bem-element-nesting-policy: 'allow' !default; + +/// +/// Context ID used for all BEM-related mixins. +/// +/// @type string +/// +$iro-bem-context-id: 'bem' !default; + +/// +/// Debug mode. +/// +/// @type bool +/// +$iro-bem-debug: false !default; + +/// +/// Colors assigned to namespaces. +/// +/// @type map +/// +$iro-bem-debug-colors: ( + object: #ffa500, + component: #00f, + layout: #ff0, + utility: #008000, + hack: #f00 +) !default; -- cgit v1.2.3-70-g09d2