Permalink
Find file Copy path
Fetching contributors…
Cannot retrieve contributors at this time
341 lines (294 sloc) 10.8 KB
@charset "UTF-8";
////
/// -------------------------------------------------------------------------------------
///
/// ext(); v1.0 - @extend on a budget
///
/// jaicab.com/sass-ext/
///
/// -------------------------------------------------------------------------------------
///
/// List all you extend budgets on the $ext-budget.
/// It is recommended to have, at least, a default budget call "total".
/// The total budget would always cover all the placeholders across the system.
///
/// You can start using this library like so:
///
/// @include ext(my_placeholder);
///
/// Then all the placeholders will be counted and you will be warned if any of them surpass the budget.
///
/// If you want to create targeted budgets, add another budget to the $ext-budget map.
/// Then use it like so:
///
/// @include ext(my_placeholder, targeted);
///
///
/// 2015 - Made with ♥ by Jaime Caballero (@jaicab_)
////
/// Placeholder budget.
/// Should be put with your other variables, before the mixins.
///
/// In 'total', set the maximun number of times you want a placeholder to be used.
/// If you want to add custom budgets, add another one right after total.
/// Total is a keyword, so it will always count all the placeholders, no matter the target.
///
/// @type Map
$ext-budget: (
'total': 15,
'heading': 12
) !default;
/// Options
/// Should be put with your other variables, before the mixins.
///
/// @prop {Bool} strict [false] - For strict development. Overrides the optional warnings and throws and error on all bad scenarios.
/// @prop {Bool} warn-over [true] - Warns you when a placeholder has been used more than budgeted.
/// @prop {Bool} warn-duplicates [true] - Warn when you use a placeholder in the same selector more than once.
/// @prop {Bool} over-only [true] - For placeholders on debug. Only show placeholders over the budget.
/// @prop {Bool} show-all [true] - For budgets debug. List an expanded version of all the used budgets, no matter of the results.
///
/// @type Map
$ext-options: (
'strict': false,
'warn-over': true,
'warn-duplicates': true,
'over-only': true,
'show-all': true
) !default;
/// Initiates the variables necessary for ext() to work
@mixin ext-init {
// OPTIONS
@if not variable-exists('ext-opt') {
// Default options
$ext-opt: (
'strict': false,
'warn-over': true,
'warn-duplicates': true,
'over-only': true,
'show-all': true
) !global;
// If the developer has set new options($ext-options), merge them with the defaults.
@if type-of($ext-options) == 'map' {
$ext-opt: map-merge($ext-opt, $ext-options) !global;
} @else {
@error 'Seems like `$ext-options` is not a map.';
}
}
// FOR EXT()
// If there is no $ext-budget variable
@if not variable-exists('ext-budget') {
@error 'You must set you placeholder budgets before using ext().';
}
// Or no budgets specified
@else if not length($ext-budget) {
@error 'You must specify at least one budget. Use `total` by default.';
}
// If the placeholder collection hasn't still been created.
@else if not variable-exists('ext-map') {
// Create map
$ext-map: () !global;
@each $key, $budget in $ext-budget {
$ext-map: map-merge($ext-map, ($key: ())) !global;
}
}
}
/// Gets an option from the options map
///
/// @param {String} $option - Name of the option
///
/// @example scss
/// ext-get-option(warn-duplicated);
/// // -> true
///
/// @returns {List} List of selectors without duplications
@function ext-get-option($option) {
$ret: ();
@if not map-has-key($ext-opt, $option) {
@error 'The option `#{$option}` is not defined or part of the options.';
} @else {
$ret: map-get($ext-opt, $option);
}
@return $ret;
}
/// Removes duplicates from a Sass list and joins them with commas
///
/// @param {List} $selectors - List of selectors
/// @param {String} $placeholder - Used placeholder. Displayed on @warn.
///
/// @example scss
/// $list: html, .foo, body, .foo, form;
/// $list: ext-remove-duplicates($list);
/// // -> html, .foo, body, form
///
/// @returns {List} List of selectors without duplications
@function ext-remove-duplicates($selectors, $placeholder) {
$ret: ();
@each $selector in $selectors {
@if not index($ret, $selector) {
$ret: append($ret, $selector, 'comma');
} @else {
// Warn if duplicated selector
$ext-message: 'The selector `#{$selector}` has already been extended for the `%#{$placeholder}` placeholder.';
@if ext-get-option('strict') {
@error $ext-message;
} @else if ext-get-option('warn-duplicates') {
@warn $ext-message;
}
}
}
@return $ret;
}
/// Lists a map of placeholders and selectors formatted for pseudoelements
///
/// @param {List} $map - Map formed by a key and a map of (placeholder, selector list)
/// @param {String} $key ['total'] - Key to budget and target a specific range of placeholders.
///
/// @example scss
/// $map: (
/// total: (
/// %foo: html, div,
/// ...
/// )
/// )
/// $list: ext-list-map($list);
///
/// @returns {String} Returns the info in a visual way as seen on ext-debug();
@function ext-list-map($map, $key: 'total') {
$ret: '';
// If it's been extended
@if length($map) > 0 {
// List the key
$over-budget: 0;
$count-selectors: 0;
$ret-loop: '';
// Loop through the placeholders
@each $placeholder, $selectors in $map {
// Count selectors
$count-selectors: length($selectors);
// List the placeholder and show use respect budget
@if length($selectors) > map-get($ext-budget, $key) or not ext-get-option('over-only') {
$ret-loop: '#{$ret-loop}#{length($selectors)}/#{map-get($ext-budget, $key)} - %#{$placeholder}';
}
// If current placeholder is under the budget
@if length($selectors) <= map-get($ext-budget, $key) {
// And we want to see it
@if not ext-get-option(over-only){
// Format for the ones under budget
$ret-loop: '#{$ret-loop}: `#{$selectors}`,\A';
}
} @else {
// Always show the ones over the budget
// Format for over budget
$ret-loop: '#{$ret-loop} => .#{$placeholder} for: `#{$selectors}`,\A';
$over-budget: $over-budget + 1;
}
}
$ext-ratio: (map-get($ext-budget, $key) * length($map));
$prefix: if($over-budget < 1 and ext-get-option('show-all'), 'All good!', '\26A0 HEY!');
$ret: '#{$prefix} - #{$over-budget} of #{length($map)} #{$key} (#{map-get($ext-budget, $key)}, #{$count-selectors}/#{$ext-ratio} ratio)';
$ret: '#{$ret} \A #{$ret-loop} \A';
}
@return $ret;
}
/// Custom `@extend` mixin
///
/// @param {String} $placeholder - Placeholder selector to extend (no `%`)
/// @param {String} $key ['total'] - Key to budget related with the placeholder.
/// @param {String} $flag [''] - Room for the !optional flag.
///
/// @example scss
/// %foo{ color: black; }
/// .something{
/// @include ext(foo);
/// }
/// // No output, but the selectors will be stored in $ext-map.
@mixin ext($placeholder, $key: 'total', $flag: '') {
// If there is a parent, it's extendable
@if not & {
@error 'You can\'t extend outside a selector.';
} @else {
// Extend would return an error if the placeholder doesn't exist
@extend %#{$placeholder} #{$flag};
// Check that the budget is there and handle in a variable
@if not variable-exists('ext-map'){
@include ext-init();
}
@if map-has-key($ext-map, $key) {
$ext-map-key: map-get($ext-map, $key) !global;
} @else{
@warn 'The budget `#{$key}` hasn\'t been specified. Please add it to your $ext-budget list.';
}
// If we don't, initialize placeholder to the current selectors
$placeholder-selectors: &; // Yes, this actually works since Sass 3.4
// If we have the budget, add one
@if map-has-key($ext-map-key, $placeholder) {
// Get the current selectors, join them with the new ones and remove the duplicates
$placeholder-selectors: join(map-get($ext-map-key, $placeholder), &, 'comma');
}
// WARN IF DUPLICATED SELECTORS
$placeholder-selectors: ext-remove-duplicates($placeholder-selectors, $placeholder);
// Update the key -> placeholder map
$ext-map-key: map-merge($ext-map-key, ($placeholder: $placeholder-selectors)) !global;
$ext-map: map-merge($ext-map, ($key: $ext-map-key)) !global;
// Update the total one
@if $key != 'total' and map-has-key($ext-map, 'total') {
$ext-map-key: map-merge($ext-map-key, ('total': $placeholder-selectors)) !global;
$ext-map: map-merge($ext-map, ('total': map-merge(map-get($ext-map, 'total'), ($placeholder: $placeholder-selectors)))) !global;
}
// Check if it's been overused
// If there are more placeholder than estimated, warn the user
@if length($ext-map-key) > 0 and length(map-get($ext-map-key, $placeholder)) > map-get($ext-budget, $key) {
$ext-message: '`%#{$placeholder}` has been used #{(length(map-get($ext-map-key, $placeholder)) - map-get($ext-budget, $key))} times more than estimated (#{map-get($ext-budget, $key)}). Check `#{&}` to prevent that.';
@if ext-get-option('strict') {
@error $ext-message;
} @else if ext-get-option('warn-over') {
@warn $ext-message;
}
}
}
}
/// Debug the results collected by ext() till being called.
/// IMPORTANT: Should be used at the end of your Sass file in order to cover all your calls to ext().
///
/// @param {String} $key-filter ['all'] - If specified, limits debug to a single budget. If not, shows all.
///
/// @example scss
/// @include ext-debug();
/// // -> fixed debug info in body:after
@mixin ext-debug($key-filter: 'all') {
body::after {
background: #635c92;
background: rgba(#635c92, 0.9);
color: #f2f2f2;
font-size: small-caption;
padding: 2em 2em 1em 2em;
text-shadow: 1px 1px 0 saturate(#069, 20%);
font-family: 'Helvetica Neue', 'Helvetica', 'Arial', sans-serif;
font-size: .9em;
position: fixed;
left: 0;
top: 0;
z-index: 100;
display: block;
// Loop through the keys
$result: '# ext() v1.0 jaicab.com/sass-ext/\A\A';
// If there's a key filter
@if $key-filter != 'all' {
@if not map-has-key($ext-map, $key-filter) {
@error 'Seems like the key `#{$key-filter}` is not on the budget or hasn\'t been used with ext().';
} @else {
$result: ext-list-map($map, $key);
}
} @else {
// Show all of them
@each $key, $map in $ext-map {
$result: $result + ext-list-map($map, $key);
}
}
@if $result == '# ext() v1.0 jaicab.com/sass-ext/\A\A'{
$result: '#{$result} Nothing to see here. Play with show-all and over-only options to see everything.\A\A ';
}
content: $result;
white-space: pre-wrap;
}
}