Skip to content

Commit

Permalink
Rename @extend @COMPOSES and constrain the context where it can be us…
Browse files Browse the repository at this point in the history
…ed to better match its semantics.
  • Loading branch information
pygy committed Jan 29, 2016
1 parent 066f97e commit d809936
Show file tree
Hide file tree
Showing 13 changed files with 250 additions and 268 deletions.
38 changes: 17 additions & 21 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,8 @@ In `sheet` mode, `j2c` follows a [**'local by default'**](https://medium.com/see
Like SASS, LESS and Stylus, `j2c` supports:

- mixins
- `@extend`
- nested selectors (in `sheet` mode)
- `@composes`, and `@extends`-like mechanism

All standard CSS at-rules are available out of the box, most importantly:

Expand Down Expand Up @@ -55,7 +55,7 @@ The [home page](http://j2c.py.gy) has a few interactive demos.
- [For building a style sheet: `j2c.sheet(rules)`](#for-building-a-style-sheet-j2csheetrules)
- [Combining multiple selectors](#combining-multiple-selectors)
- [At-rules](#at-rules)
- [Mixins and @extend](#mixins-and-extend)
- [Mixins and @composes](#mixins-and-composes)
- [CSS Hacks](#css-hacks)
- [Inserting a stylesheet in a document](#inserting-the-stylesheet-in-the-document)
- [Isomorphic app support](#isomorphic-app-support)
Expand Down Expand Up @@ -380,57 +380,53 @@ becomes

For `@keyframes` rules, a `@-webkit-keyframes` block is automatically created with auto-prefixed property names.

#### Mixins and `@extend`
#### Mixins and `@coposes`

Mixins and `@extend` make `j2c` sheets composable. Both techniques can be combined.
Mixins and `@composes` make `j2c` sheets composable. Both techniques can be combined.

##### Mixins and source objects composition

For mixins, arrays works the same way at the selector level as they do at the property/value one. You can therefore use the [method described in the "inline" section](#mixins) to create mixins, that can return either at-rules, selectors, properties or a mix thereof.

##### `@extend`
##### `@composes`

`j2c` also supports a SASS-like `@extend`, more powerful in some regards, but more limited in others.
`j2c` also supports `@composes`, which works a bit like the SASS`@extend`, more powerful in some regards, but more limited in others.

The limitation is that it can only deal with classes. Specifically:

```JS
namespace = j2c.sheet({
'.red': {color: '#f00'}
})

sheet = j2c.sheet(namespace, {
sheet = j2c.sheet({
'.red': {
color: '#f00'
},
'.great': {
fontSize: '3em'
},
'.greatRed': {
'@extend': ['.great', '.red'] // you can also pass a single class
// `scarlet` here is the target of the composition, `great` and `red` are the sources.
'.scarlet': {
'@composes': ['.great', '.red'] // you can also pass a single class
}
})
```

`sheet.greatRed` is now defined as `'great_j2c... red_j2c... greatRed_j2c...'` (class names truncated for readability).
`sheet.scarlet` is now defined as `'great__j2c-xxx red__j2c-xxx scarlet__j2c-xxx'` (class names truncated for readability).

The extra power comes from the fact that you can inherit from arbitrary classes, not just j2c-defined ones:

```JS
sheet = j2c.sheet(namespace, {
'.myButton': {
'@extend': ':global(.button)', // coming, say, form Bootstrap
'@composes': ':global(.button)', // coming, say, form Bootstrap
color: theme.highlight
}
})
```

Here, `sheet.myButton` is `'button myButton_j2c...'`.

While `@extend` can import from arbitrary classes, it only imports into local ones.

`@extend` works fine with nested selectors. If there are more than one class in a selector, `@extend` applies to the last (right-most) one.

###### Invalid uses
While the `@composes` sources can be arbitrary classes, the target must be a local one. It will not work in global context.

If the last or only selector is a `:global(.klass)`, in `@global` context, or in the absence of a class in the selector, `@extend` is turned into a `at-extend` property and inserted as-is in the sheet.
`@composes` doesn't support nested selectors, and doesn't work in conditional at rules. Its target must lie at the first nesting level.

#### CSS Hacks

Expand Down
71 changes: 32 additions & 39 deletions dist/j2c.amd.js
Original file line number Diff line number Diff line change
Expand Up @@ -146,8 +146,6 @@ define(function () { 'use strict';
}
}

var findClass = /()(?::global\(\s*(\.[-\w]+)\s*\)|(\.)([-\w]+))/g

/**
* Hanldes at-rules
*
Expand All @@ -157,7 +155,7 @@ define(function () { 'use strict';
* @param {string[]} v - Either parameters for block-less rules or their block
* for the others.
* @param {string} prefix - the current selector or a prefix in case of nested rules
* @param {string} rawPrefix - as above, but without localization transformations
* @param {string} composes - as above, but without localization transformations
* @param {string} vendors - a list of vendor prefixes
* @Param {boolean} local - are we in @local or in @global scope?
* @param {object} ns - helper functions to populate or create the @local namespace
Expand All @@ -166,8 +164,8 @@ define(function () { 'use strict';
* @param {function} ns.l - @local helper
*/

function at(k, v, buf, prefix, rawPrefix, vendors, local, ns){
var kk, i
function at(k, v, buf, prefix, composes, vendors, local, ns){
var i, kk
if (/^@(?:namespace|import|charset)$/.test(k)) {
if(type.call(v) == ARRAY){
for (kk = 0; kk < v.length; kk++) {
Expand All @@ -185,38 +183,35 @@ define(function () { 'use strict';
// add a @-webkit-keyframes block too.

buf.a('@-webkit-', k.slice(1), ' {\n')
sheet(v, buf, '', '', ['webkit'])
sheet(v, buf, '', 1, ['webkit'])
buf.c('}\n')

buf.a(k, ' {\n')
sheet(v, buf, '', '', vendors, local, ns)
sheet(v, buf, '', 1, vendors, local, ns)
buf.c('}\n')

} else if (/^@extends?$/.test(k)) {
} else if (/^@composes$/.test(k)) {
if (!local) {
buf.c('@-error-cannot-extend-in-global-context ', JSON.stringify(rawPrefix), ';\n')
buf.a('@-error-at-composes-in-at-global;\n')
return
}
if (!composes) {
buf.a('@-error-at-composes-no-nesting;\n')
return
}
rawPrefix = splitSelector(rawPrefix)
for(i = 0; i < rawPrefix.length; i++) {
/*eslint-disable no-cond-assign*/
// pick the last class to be extended
while (kk = findClass.exec(rawPrefix[i])) k = kk[4]
/*eslint-enable no-cond-assign*/
composes = splitSelector(composes)
for(i = 0; i < composes.length; i++) {
k = /^\s*\.(\w+)\s*$/.exec(composes[i])
if (k == null) {
// the last class is a :global(.one)
buf.c('@-error-cannot-extend-in-global-context ', JSON.stringify(rawPrefix[i]), ';\n')
continue
} else if (/^@extends?$/.test(k)) {
// no class in the selector, therefore `k` hasn't been overwritten.
buf.c('@-error-no-class-to-extend-in ', JSON.stringify(rawPrefix[i]), ';\n')
buf.a('@-error-at-composes-bad-target ', JSON.stringify(composes[i]), ';\n')
continue
}
ns.e(
ns.c(
type.call(v) == ARRAY ? v.map(function (parent) {
return parent.replace(/()(?::global\(\s*(\.[-\w]+)\s*\)|()\.([-\w]+))/, ns.l)
}).join(' ') : v.replace(/()(?::global\(\s*(\.[-\w]+)\s*\)|()\.([-\w]+))/, ns.l),
k
return parent.replace(/()(?::?global\(\s*\.?([-\w]+)\s*\)|()\.([-\w]+))/, ns.l)
}).join(' ') : v.replace(/()(?::?global\(\s*\.?([-\w]+)\s*\)|()\.([-\w]+))/, ns.l),
k[1]
)
}
} else if (/^@(?:font-face$|viewport$|page )/.test(k)) {
Expand All @@ -233,14 +228,14 @@ define(function () { 'use strict';
}

} else if (/^@global$/.test(k)) {
sheet(v, buf, prefix, rawPrefix, vendors, 0, ns)
sheet(v, buf, prefix, 1, vendors, 0, ns)

} else if (/^@local$/.test(k)) {
sheet(v, buf, prefix, rawPrefix, vendors, 1, ns)
sheet(v, buf, prefix, 1, vendors, 1, ns)

} else if (/^@(?:media |supports |document )./.test(k)) {
buf.a(k, ' {\n')
sheet(v, buf, prefix, rawPrefix, vendors, local, ns)
sheet(v, buf, prefix, 1, vendors, local, ns)
buf.c('}\n')

} else {
Expand All @@ -254,22 +249,22 @@ define(function () { 'use strict';
* @param {array|string|object} statements - a source object or sub-object.
* @param {string[]} buf - the buffer in which the final style sheet is built
* @param {string} prefix - the current selector or a prefix in case of nested rules
* @param {string} rawPrefix - as above, but without localization transformations
* @param {string} composes - the potential target of a @composes rule, if any.
* @param {string} vendors - a list of vendor prefixes
* @Param {boolean} local - are we in @local or in @global scope?
* @param {object} ns - helper functions to populate or create the @local namespace
* and to @extend classes
* @param {function} ns.e - @extend helper
* and to @composes classes
* @param {function} ns.e - @composes helper
* @param {function} ns.l - @local helper
*/
function sheet(statements, buf, prefix, rawPrefix, vendors, local, ns) {
var k, kk, v, inDeclaration
function sheet(statements, buf, prefix, composes, vendors, local, ns) {
var k, v, inDeclaration

switch (type.call(statements)) {

case ARRAY:
for (k = 0; k < statements.length; k++)
sheet(statements[k], buf, prefix, rawPrefix, vendors, local, ns)
sheet(statements[k], buf, prefix, composes, vendors, local, ns)
break

case OBJECT:
Expand All @@ -285,15 +280,15 @@ define(function () { 'use strict';
// Handle At-rules
inDeclaration = (inDeclaration && buf.c('}\n') && 0)

at(k, v, buf, prefix, rawPrefix, vendors, local, ns)
at(k, v, buf, prefix, composes, vendors, local, ns)

} else {
// selector or nested sub-selectors

inDeclaration = (inDeclaration && buf.c('}\n') && 0)

sheet(v, buf,
(kk = /,/.test(prefix) || prefix && /,/.test(k)) ?
(/,/.test(prefix) || prefix && /,/.test(k)) ?
cartesian(splitSelector(prefix), splitSelector( local ?
k.replace(
/()(?::global\(\s*(\.[-\w]+)\s*\)|(\.)([-\w]+))/g, ns.l
Expand All @@ -304,9 +299,7 @@ define(function () { 'use strict';
/()(?::global\(\s*(\.[-\w]+)\s*\)|(\.)([-\w]+))/g, ns.l
) : k
), prefix),
kk ?
cartesian(splitSelector(rawPrefix), splitSelector(k), rawPrefix).join(',') :
concat(rawPrefix, k, rawPrefix),
composes || prefix ? '' : k,
vendors,
local, ns
)
Expand Down Expand Up @@ -397,7 +390,7 @@ define(function () { 'use strict';
}

var state = {
e: function extend(parent, child) {
c: function composes(parent, child) {
var nameList = locals[child]
locals[child] =
nameList.slice(0, nameList.lastIndexOf(' ') + 1) +
Expand Down
2 changes: 1 addition & 1 deletion dist/j2c.amd.min.js

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

0 comments on commit d809936

Please sign in to comment.