Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Next Next commit
working
  • Loading branch information
Kevin Yang committed Aug 15, 2016
commit aa9bb771bf017b0ad001ac1fb09190f6af80d91b
12 changes: 12 additions & 0 deletions addon/helpers/lookup-module-styles.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import Ember from 'ember';

export function lookupModuleStyles([classStyles, stylesMap, localClassStyles]) {
const classStylesArray = classStyles ? classStyles.split(' ') : [];
const appliedlocalClassStylesArray = localClassStyles.split(' ')
.map(style => stylesMap[style])
.filter(style => style);

return classStylesArray.concat(appliedlocalClassStylesArray).join(' ');
}

export default Ember.Helper.helper(lookupModuleStyles);
1 change: 1 addition & 0 deletions app/helpers/lookup-module-styles.js
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { default, lookupModuleStyles } from 'ember-css-modules/helpers/lookup-module-styles';
90 changes: 50 additions & 40 deletions lib/htmlbars-plugin/index.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
'use strict';

var utils = require('./utils');
var htmlbars = require('htmlbars/dist/cjs/htmlbars-syntax');


function ClassTransformPlugin() {
this.syntax = null;
Expand All @@ -14,6 +16,10 @@ ClassTransformPlugin.prototype.transform = function(ast) {
this.builders = this.syntax.builders;
}

this.getPath = this.builders.path('lookup-module-styles');
this.unboundPath = this.builders.path('unbound');
this.stylePath = this.builders.path('styles');

var plugin = this;
var walker = new this.syntax.Walker();

Expand All @@ -35,33 +41,51 @@ ClassTransformPlugin.prototype.transform = function(ast) {
ClassTransformPlugin.prototype.transformStatement = function(node) {
var localClassPair = utils.getPair(node, 'local-class');
if (!localClassPair) { return; }
if (!~['StringLiteral', 'TextNode', 'SubExpression', 'MustacheStatement'].indexOf(localClassPair.value.type)) {
throw new TypeError('ember-css-modules - invalid type, ' + localClassPair.value.type + ', passed to local-class attribute.');
}

utils.removePair(node, localClassPair);

var classPair = utils.getPair(node, 'class');
var params = [];
var concatSexpr;
var classPart;

if (classPair) {
params.push(classPair.value);
if (classPair.value.type === 'PathExpression') {
classPart = this.builders.string(classPair.value.original);
} else {
classPart = classPair.value;
}

utils.removePair(node, classPair);
} else {
classPart = this.builders.null();
}

var localClassPart;
if (~['SubExpression', 'MustacheStatement'].indexOf(localClassPair.value.type)) {
localClassPart = localClassPair.value;
} else {
localClassPart = this.builders['string'](this.trimWhitespace(localClassPair.value.value));
}

utils.pushAll(params, this.localToPath(localClassPair.value));
this.divide(params);
concatSexpr = this.builders.sexpr('concat', params);
node.hash.pairs.push(this.builders.pair('class', concatSexpr));
var unboundPart = this.builders['sexpr'](this.unboundPath, [this.stylePath]);
var finalPart = this.builders['sexpr'](this.getPath, [classPart, unboundPart, localClassPart]);
node.hash.pairs.push(this.builders.attr('class', finalPart));
};

ClassTransformPlugin.prototype.transformElementNode = function(node) {
var localClassAttr = utils.getAttr(node, 'local-class');
if (!localClassAttr) { return; }
if (!~['StringLiteral', 'TextNode', 'SubExpression', 'MustacheStatement'].indexOf(localClassAttr.value.type)) {
throw new TypeError('ember-css-modules - invalid type, ' + localClassAttr.value.type + ', passed to local-class attribute.');
}

utils.removeAttr(node, localClassAttr);

var classAttr = utils.getAttr(node, 'class');
var parts = [];
var classAttrValue
var classAttrValue;
var classPart;

if (classAttr) {
utils.removeAttr(node, classAttr);
Expand All @@ -71,49 +95,35 @@ ClassTransformPlugin.prototype.transformElementNode = function(node) {
// the attr value into parts that make up the ConcatStatement
// that is returned from this method
if (classAttrValue.type === 'ConcatStatement') {
parts.push(this.builders.sexpr('concat', classAttrValue.parts));
classPart = this.builders.sexpr('concat', classAttrValue.parts);
} else if (classAttrValue.type === 'TextNode') {
parts.push(this.builders.string(classAttrValue.chars));
classPart = this.builders.string(classAttrValue.chars);
} else if (classAttrValue.type === 'MustacheStatement') {
if (classAttrValue.params.length) {
parts.push(classAttrValue);
classPart = classAttrValue;
} else {
parts.push(this.builders.path(classAttrValue.path.original));
classPart = this.builders.path(classAttrValue.path.original);
}
}
} else {
classPart = this.builders.null();
}

utils.pushAll(parts, this.localToPath(localClassAttr.value));
this.divide(parts);
node.attributes.push(this.builders.attr('class', this.builders.concat(parts)));
};

ClassTransformPlugin.prototype.localToPath = function(node) {
if (!~['StringLiteral', 'TextNode'].indexOf(node.type)) {
throw new TypeError('ember-css-modules - invalid type, ' + node.type + ', passed to local-class attribute.');
var localClassPart;
if (~['SubExpression', 'MustacheStatement'].indexOf(localClassAttr.value.type)) {
localClassPart = localClassAttr.value;
} else {
localClassPart = this.builders['string'](this.trimWhitespace(localClassAttr.value.chars));
}

var locals = typeof node.chars === 'string' ? node.chars : node.value;
var builder = typeof node.chars === 'string' ? 'mustache' : 'sexpr';
var classes = locals.split(' ');

return classes.filter(function(local) {
return local.trim().length > 0;
}).map(function(local) {
var unboundPath = this.builders.path('unbound');
var stylePath = this.builders.path('styles.' + local.trim());
return this.builders[builder](unboundPath, [stylePath]);
}, this);
};
var unboundPart = this.builders['sexpr'](this.unboundPath, [this.stylePath]);
var finalPart = this.builders['mustache'](this.getPath, [classPart, unboundPart, localClassPart]);

ClassTransformPlugin.prototype.divide = function(parts) {
for (var i=0;i<parts.length;i++) {
if (i % 2 === 1) {
parts.splice(i, 0, this.builders.string(' '));
}
}
node.attributes.push(this.builders.attr('class', finalPart));
};

return parts;
ClassTransformPlugin.prototype.trimWhitespace = function(str) {
return str.replace(/\s+/g,' ').trim() // " foo bar " -> "foo bar"
};

module.exports = ClassTransformPlugin;
69 changes: 53 additions & 16 deletions tests-node/htmlbars-plugin-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,54 +2,91 @@ var test = require('tape');
var htmlbars = require('htmlbars/dist/cjs/htmlbars-syntax');
var ClassTransformPlugin = require('../lib/htmlbars-plugin');


testTransformation('creating a class attribute', {
input: 'class="foo"',
statementOutput: 'class="foo"',
elementOutput: 'class="foo"'
});

testTransformation('creating a local-class attribute', {
input: 'local-class="foo"',
statementOutput: 'class=(concat (unbound styles.foo))', // FIXME superfluous concat
elementOutput: 'class="{{unbound styles.foo}}"'
statementOutput: 'class=(lookup-module-styles null (unbound styles) "foo")',
elementOutput: 'class={{lookup-module-styles null (unbound styles) "foo"}}'
});

testTransformation('creating a class attribute with multiple classes', {
input: 'local-class="foo bar"',
statementOutput: 'class=(concat (unbound styles.foo) " " (unbound styles.bar))',
elementOutput: 'class="{{unbound styles.foo}} {{unbound styles.bar}}"'
statementOutput: 'class=(lookup-module-styles null (unbound styles) "foo bar")',
elementOutput: 'class={{lookup-module-styles null (unbound styles) "foo bar"}}'
});

testTransformationElement('creating a class attribute with dynamic value', {
input: 'local-class={{if true foo}}',
statementOutput: 'class=(lookup-module-styles null (unbound styles) (if true foo))',
elementOutput: 'class={{lookup-module-styles null (unbound styles) {{if true foo}}}}'
});

testTransformation('appending to a class attribute', {
input: 'class="x" local-class="foo"',
statementOutput: 'class=(concat "x" " " (unbound styles.foo))',
elementOutput: 'class="x {{unbound styles.foo}}"'
statementOutput: 'class=(lookup-module-styles "x" (unbound styles) "foo")',
elementOutput: 'class={{lookup-module-styles "x" (unbound styles) "foo"}}'
});

testTransformation('appending to a class attribute with multiple classes', {
input: 'class="x" local-class="foo bar"',
statementOutput: 'class=(concat "x" " " (unbound styles.foo) " " (unbound styles.bar))',
elementOutput: 'class="x {{unbound styles.foo}} {{unbound styles.bar}}"'
statementOutput: 'class=(lookup-module-styles "x" (unbound styles) "foo bar")',
elementOutput: 'class={{lookup-module-styles "x" (unbound styles) "foo bar"}}'
});

testTransformation('appending to an unquoted class attribute', {
input: 'class=x local-class="foo"',
statementOutput: 'class=(concat x " " (unbound styles.foo))',
elementOutput: 'class="x {{unbound styles.foo}}"'
statementOutput: 'class=(lookup-module-styles "x" (unbound styles) "foo")',
elementOutput: 'class={{lookup-module-styles "x" (unbound styles) "foo"}}'
});

testTransformation('appending to an unquoted class attribute with multiple classes', {
input: 'class=x local-class="foo bar"',
statementOutput: 'class=(concat x " " (unbound styles.foo) " " (unbound styles.bar))',
elementOutput: 'class="x {{unbound styles.foo}} {{unbound styles.bar}}"'
statementOutput: 'class=(lookup-module-styles "x" (unbound styles) "foo bar")',
elementOutput: 'class={{lookup-module-styles "x" (unbound styles) "foo bar"}}'
});

function testTransformation(title, options) {
test('ClassTransformPlugin: ' + title, function(assert) {
assert.plan(3);
assertTransforms('<div [ATTRS]></div>', 'ElementNode', options.input, options.elementOutput, assert);
assertTransforms('{{x-div [ATTRS]}}', 'MustacheStatement', options.input, options.statementOutput, assert);
assertTransforms('{{#x-div [ATTRS]}}content{{/x-div}}', 'BlockStatement', options.input, options.statementOutput, assert);
assertElementTransforms('<div [ATTRS]></div>', 'ElementNode', options.input, options.elementOutput, assert);
assertStatementTransforms('{{x-div [ATTRS]}}', 'MustacheStatement', options.input, options.statementOutput, assert);
assertStatementTransforms('{{#x-div [ATTRS]}}content{{/x-div}}', 'BlockStatement', options.input, options.statementOutput, assert);
});
}

function testTransformationElement(title, options) {
test('ClassTransformPlugin: ' + title, function(assert) {
assert.plan(1);
assertElementTransforms('<div [ATTRS]></div>', 'ElementNode', options.input, options.elementOutput, assert);
});
}

function testTransformationStatement(title, options) {
test('ClassTransformPlugin: ' + title, function(assert) {
assert.plan(2);
assertStatementTransforms('{{x-div [ATTRS]}}', 'MustacheStatement', options.input, options.statementOutput, assert);
assertStatementTransforms('{{#x-div [ATTRS]}}content{{/x-div}}', 'BlockStatement', options.input, options.statementOutput, assert);
});
}

function assertTransforms(template, type, input, output, assert) {
function assertElementTransforms(template, type, input, output, assert) {
var input = template.replace('[ATTRS]', input);
var output = template.replace('[ATTRS]', output);

var actual = htmlbars.print(htmlbars.parse(input, { plugins: { ast: [ClassTransformPlugin] } }));
assert.equal(actual, output, 'works for ' + type);
}

function assertStatementTransforms(template, type, input, output, assert) {
var input = template.replace('[ATTRS]', input);
var output = template.replace('[ATTRS]', output);

var actual = htmlbars.print(htmlbars.parse(input, { plugins: { ast: [ClassTransformPlugin] } }));
var expected = htmlbars.print(htmlbars.parse(output));
assert.equal(actual, expected, 'works for ' + type);
Expand Down
62 changes: 62 additions & 0 deletions tests/unit/helpers/lookup-module-styles-test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
import { lookupModuleStyles } from 'dummy/helpers/lookup-module-styles';
import { module, test } from 'qunit';

module('Unit | Helper | lookup-module-styles');

test('applying local-class', function(assert) {
let stylesMap = {
foo: '_foo_123',
bar: '_bar_456'
}

let classNames = lookupModuleStyles([null, stylesMap, 'foo']);
assert.equal(classNames, '_foo_123');
});

test('applying local-class to non-existent map', function(assert) {
let stylesMap = {
foo: '_foo_123',
bar: '_bar_456'
}

let classNames = lookupModuleStyles([null, stylesMap, 'baz']);
assert.equal(classNames, '');
});

test('applying to no style map', function(assert) {
let stylesMap = {
}

let classNames = lookupModuleStyles(['a', stylesMap, 'baz']);
assert.equal(classNames, 'a');
});

test('applying class and multiple local-class', function(assert) {
let stylesMap = {
foo: '_foo_123',
bar: '_bar_456'
}

let classNames = lookupModuleStyles(['a', stylesMap, 'foo bar']);
assert.equal(classNames, 'a _foo_123 _bar_456');
});

test('applying multiple local-class', function(assert) {
let stylesMap = {
foo: '_foo_123',
bar: '_bar_456'
}

let classNames = lookupModuleStyles([null, stylesMap, 'foo bar']);
assert.equal(classNames, '_foo_123 _bar_456');
});

test('applying multiple class and multiple local-class', function(assert) {
let stylesMap = {
foo: '_foo_123',
bar: '_bar_456'
}

let classNames = lookupModuleStyles(['a b', stylesMap, 'foo bar']);
assert.equal(classNames, 'a b _foo_123 _bar_456');
});