I need to write a html5 template with different "flavors" or color-variants.
I would like to have one scss file to work on but several css files to be rendered.
Lets say my scss entry point is app.scss
IMHO the ideal approach would be something like:
$flavors: (
flavor-a: (
background: white
),
flavor-b: (
background: grey
)
);
@mixin flavor($name) {
/* parser-rule-start */
@content;
/* parser-rule-end */
}
html {
/* regular rule - valid for all flavors => goes to app.css */
font-family: sans-serif;
@each $name, $options in $flavors {
@include flavor($name) {
/* flavor-rule => goes to flavor-a.css / flavor-b.css */
background: map-get($options, 'background');
}
}
}
so i end up with
I had that requirement before and solved it with multiple entry files, that call mixins for colorization and so on.
But i dont like that approach because after i code the scss for a new component, i would need to move chunks of lines from the structure file to the flavor-mixin, that is called in the flavor-*.scss entry files.
ATM my build looks like (gulp):
/**
* compile/concat scss
*/
gulp.task('css', function () {
const sassOptions = {
outputStyle: "compressed",
errorLogToConsole: true
};
const autoprefixerOptions = {
browsersList: [
"last 2 versions",
"ie >= 11"
]
};
return gulp
.src("src/scss/*.scss")
.pipe(sourcemaps.init())
.pipe(sass(sassOptions).on('error', makeErrorLogger('css')))
.pipe(autoprefixer(autoprefixerOptions))
// .pipe(splitFlavors()) <- the point i would need some magic
.pipe(sourcemaps.write('.'))
.pipe(gulp.dest("public/static/css"))
.pipe(browserSync.stream({match: '**/*.css'}));
});
Does someone know a gulp plugin for that purpose or do i have to code it myself?
UPDATE 3
another adoption for replacement
let newSelector = selector.replace(/\s?\:\:flavor\-([^\s]+)/g, "").trim();
UPDATE 2
the replacement in the selector of the css rule needs to be adopted
let newSelector = selector.replace(/\s?\:\:flavor\-([a-zA-Z0-9\-\_\s]+)/g, "").trim();
UPDATE
by using a modified selector rather than properties you can continue using nested rules inside a "for-flavor" block.
So adjust the mixin to look like:
@mixin for-flavor($name) {
::flavor-#{$name} & {
@content;
}
}
and the gulp task:
const path = require("path");
const gulp = require("gulp");
const sourcemaps = require("gulp-sourcemaps");
const sass = require("gulp-sass");
const autoprefixer = require("gulp-autoprefixer");
const through = require("through2");
const postcss = require("gulp-postcss");
/**
* compile/concat scss
*/
gulp.task('css', function () {
const sassOptions = {
outputStyle: "compressed",
errorLogToConsole: true
};
const autoprefixerOptions = {
browsersList: [
"last 2 versions",
"ie >= 11"
]
};
function addFlavorFiles() {
return through.obj(function(file, encoding, callback) {
/* @var file File */
let content = file.contents.toString();
let names = [];
let matches = content.match(/\:\:flavor\-([^\s\{]+)/g);
if (matches) {
names = matches.map(match => match.replace(/\:\:flavor\-/, '').trim());
// unique
names = names.filter((el, index, arr) => {
return index === arr.indexOf(el);
});
}
names.forEach(name => {
let newFile = file.clone();
newFile.contents = Buffer.concat([Buffer.from(`/*!flavor:${name}*/\n`, encoding), file.contents]);
let filePath = path.parse(file.path);
newFile.path = path.join(filePath.dir, `flavor-${name + filePath.ext}`);
this.push(newFile);
});
callback(null, file);
})
}
function filterFlavors(css, opts) {
let flavor = null;
if (css.nodes[0].type === "comment" && css.nodes[0].text.indexOf('!flavor:') === 0) {
flavor = css.nodes[0].text.replace(/^\!flavor\:/, "").trim();
}
css.walkRules(rule => {
let selector = rule.selector;
if (/^\:\:flavor\-/.test(selector)) {
// flavor rule
if (flavor === null) {
// general file, all flavor rules must go...
rule.remove();
} else {
let matches = selector.match(/\:\:flavor\-([a-zA-Z0-9\-\_]+)/);
let currentFlavor = matches[1];
if (flavor !== currentFlavor) {
// wrong flavor
rule.remove();
} else {
// keep rule but adjust selector
let newSelector = selector.replace(/^\:\:flavor\-([a-zA-Z0-9\-\_]+)/, "").trim();
rule.selector = newSelector;
}
}
} else if(flavor !== null) {
// general rule but flavor file, so remove the rule
rule.remove();
}
});
css.walkRules(rule => {
if (!rule.nodes || rule.nodes.length === 0) {
rule.remove();
}
});
css.walkAtRules(atRule => {
if (!atRule.nodes || atRule.nodes.length === 0) {
atRule.remove();
}
});
// optional: delete all font-face definitions from flavor file
if (flavor !== null) {
css.walkAtRules(atRule => {
if (atRule.name === "font-face") {
atRule.remove();
}
});
}
}
return gulp
.src("src/scss/*.scss")
.pipe(sourcemaps.init())
.pipe(sass(sassOptions).on('error', makeErrorLogger('css')))
.pipe(addFlavorFiles())
.pipe(autoprefixer(autoprefixerOptions))
.pipe(postcss([filterFlavors]))
.pipe(sourcemaps.write('.'))
.pipe(gulp.dest("public/static/css"))
});
ORIGINAL POST
I managed to solve it like i intended to
Use a sass mixin, that adds rules for parsing:
@mixin for-flavor($name) {
-flavor-start: unquote($name);
@content;
-flavor-end: unquote($name);
}
use it in for your css declarations
// you get the idea...
$flavors: (
"lightblue": (
background: lightblue,
),
"pink": (
background: pink,
),
"dark": (
background: black
),
);
#page-header {
background: black;
@each $name, $options in $flavors {
@if map_has_key($options, 'background') {
@include for-flavor($name) {
background: map_get($options, 'background');
}
}
}
@include for-flavor("lightblue") {
/* special rule for specific flavor */
color: black;
}
}
gulp
const path = require("path");
const gulp = require("gulp");
const sourcemaps = require("gulp-sourcemaps");
const sass = require("gulp-sass");
const autoprefixer = require("gulp-autoprefixer");
const through = require("through2");
const postcss = require("gulp-postcss");
/**
* compile/concat scss
*/
gulp.task('css', function () {
const sassOptions = {
outputStyle: "compressed",
errorLogToConsole: true
};
const autoprefixerOptions = {
browsersList: [
"last 2 versions",
"ie >= 11"
]
};
function addFlavorFiles() {
return through.obj(function(file, encoding, callback) {
/* @var file File */
let content = file.contents.toString();
let names = [];
let matches = content.match(/\-flavor\-start\:([^\;]+)/g);
if (matches) {
names = matches.map(match => match.replace(/\-flavor\-start\:/, '').trim());
}
names.forEach(name => {
let newFile = file.clone();
newFile.contents = Buffer.concat([Buffer.from(`/*!flavor:${name}*/\n`, encoding), file.contents]);
let filePath = path.parse(file.path);
newFile.path = path.join(filePath.dir, `flavor-${name + filePath.ext}`);
this.push(newFile);
});
callback(null, file);
})
}
function filterFlavors(css, opts) {
let flavor = null;
if (css.nodes[0].type === "comment" && css.nodes[0].text.indexOf('!flavor:') === 0) {
flavor = css.nodes[0].text.replace(/^\!flavor\:/, "").trim();
}
let inFlavorBlock = "";
css.walkDecls(decl => {
let prop = decl.prop;
let isControlProp = false;
let value = decl.value.trim();
if (prop === "-flavor-end") {
inFlavorBlock = "";
isControlProp = true;
} else if (prop === "-flavor-start") {
inFlavorBlock = value;
isControlProp = true;
}
let isValid = ((inFlavorBlock === "" && flavor === null) || inFlavorBlock === flavor);
if (isValid === false || isControlProp) {
decl.remove();
}
});
css.walkRules(rule => {
if (!rule.nodes || rule.nodes.length === 0) {
rule.remove();
}
});
css.walkAtRules(atRule => {
if (!atRule.nodes || atRule.nodes.length === 0) {
atRule.remove();
}
});
}
return gulp
.src("src/scss/*.scss")
.pipe(sourcemaps.init())
.pipe(sass(sassOptions))
.pipe(addFlavorFiles())
.pipe(autoprefixer(autoprefixerOptions))
.pipe(postcss([filterFlavors]))
.pipe(sourcemaps.write('.'))
.pipe(gulp.dest("public/static/css"))
});
The only remaining problem is, that the sourcemaps are not filtered...