Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

BREAKING: Rewrite theme build and support scopes (Belize Themes) #10

Merged
merged 1 commit into from
Mar 23, 2017
Merged
Show file tree
Hide file tree
Changes from all commits
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
52 changes: 37 additions & 15 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,9 +18,14 @@ npm install less-openui5
```js
var lessOpenUI5 = require('less-openui5');

lessOpenUI5.build({
// Create a builder instance
var builder = new lessOpenUI5.Builder();

// Build a theme
builder.build({
lessInput: '@var: #ffffff; .class { color: @var; float: left }'
}, function(err, result) {
})
.then(function(result) {

console.log(result.css); // => regular css
/*
Expand Down Expand Up @@ -48,22 +53,41 @@ lessOpenUI5.build({
[]
*/

// Clear builder cache when finished to cleanup memory
builder.clearCache();

});
```

## API

### build(options, callback)
### new Builder()

Creates a new `Builder` instance.

It caches build results to only rebuild a theme when related files have been changed.
This is mainly relevant when building themes as part of a server middleware like [`connect-openui5`](https://github.com/SAP/connect-openui5).

### .build(options)
Returns a Promise resolving with a [`result`](#result) object.

#### options

##### lessInput

*Required*
*Required (either `lessInput` or `lessInputPath`, not both)*
Type: `string`

Input less content.

##### lessInputPath

*Required (either `lessInput` or `lessInputPath`, not both)*
Type: `string`

Path to input less file.
When `rootPaths` is given this must be a relative path inside one of the provided `rootPaths`, otherwise just a regular filesystem path.

##### rtl

Type: `boolean`
Expand All @@ -79,9 +103,6 @@ Root paths to use for import directives.

This option differs from the less `compiler.paths` option.
It is useful if less files are located in separate folders but referenced as they would all be in one.
If `rootPaths` are provided and a file can not be found, the `compiler.paths` option will be used instead.

*Note:* `parser.filename` has to be set to the path of the `input` file in order to get this working.

###### Example

Expand Down Expand Up @@ -128,35 +149,36 @@ Type `string`
Dot-separated name of the corresponding library.
It will be used to inline the `variables` JSON as data-uri which can be retrieved at runtime.

#### callback(error, result)

*Required*
Type: `function`
#### result

##### result.css
##### css

Type: `string`

Regular css output.

##### result.cssRtl
##### cssRtl

Type: `string`

Mirrored css for right-to-left support (if rtl option was enabled).

##### result.variables
##### variables

Type: `object`

Key-value map of all global less variables (without @ prefix).

##### result.imports
##### imports

Type: `array`

Paths to files imported via import directives.

### .clearCache()
Clears all cached build results.
Use this method to prevent high memory consumption when building many themes within the same process.

## Contributing
See [CONTRIBUTING.md](CONTRIBUTING.md).

Expand Down
162 changes: 162 additions & 0 deletions lib/diff.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,162 @@
// Copyright 2017 SAP SE.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http: //www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND,
// either express or implied. See the License for the specific
// language governing permissions and limitations under the License.

'use strict';

// Regular expression to match type of property in order to check whether
// it possibly contains color values.
var rProperties = /(color|background|border|text|outline)(?!\-(width|radius|offset|style|align|overflow|transform))(\-(color|shadow|image))?/;

function selectorEquals(s1, s2) {

// Make sure there is the same number of select parts
if (s1.length !== s2.length) {
return false;
}

// Check if all the parts are the same strings
for (var i = 0; i < s1.length; i++) {
if (s1[i] !== s2[i]) {
return false;
}
}

return true;
}

function Diffing(oBase, oCompare) {
this.oBase = oBase;
this.oCompare = oCompare;

this.oDiff = {
type: "stylesheet",
stylesheet: {
rules: []
},
};

this.oStack = {
type: "stylesheet",
stylesheet: {
rules: []
},
};
}

Diffing.prototype.diffRules = function(oBaseRules, oCompareRules) {
var aDiffRules = [];
var iBaseNode, iCompareNode;

for (iBaseNode = 0, iCompareNode = 0; iBaseNode < oBaseRules.length; iBaseNode++, iCompareNode++) {
var oBaseNode = oBaseRules[iBaseNode];
var oCompareNode = oCompareRules[iCompareNode];
var oDiffNode = null;

// Add all different compare nodes to stack and check for next one
while (oBaseNode.type !== oCompareNode.type) {
this.oStack.stylesheet.rules.push(oCompareNode);
iCompareNode++;
oCompareNode = oCompareRules[iCompareNode];
}

if (oBaseNode.type === "comment") {
var sBaseComment = oBaseNode.comment;
var sCompareComment = oCompareNode.comment;

if (sBaseComment !== sCompareComment) {
oDiffNode = oCompareNode;
}
}

if (oBaseNode.type === "rule") {

// Add all rules with different selector to stack and check for next one
while (!selectorEquals(oBaseNode.selectors, oCompareNode.selectors)) {
this.oStack.stylesheet.rules.push(oCompareNode);
iCompareNode++;
oCompareNode = oCompareRules[iCompareNode];
}

var aBaseDeclarations = oBaseNode.declarations;
var aCompareDeclarations = oCompareNode.declarations;
for (var j = 0; j < aBaseDeclarations.length; j++) {
var oBaseDeclaration = aBaseDeclarations[j];
var oCompareDeclaration = aCompareDeclarations[j];

if (oBaseDeclaration.type === "declaration") {

// TODO: Also check for different node and add to stack???
if (oBaseDeclaration.type === oCompareDeclaration.type) {

if (oBaseDeclaration.property === oCompareDeclaration.property) {

// Always add color properties to diff to prevent unexpected CSS overrides
// due to selectors with more importance
if (oBaseDeclaration.value !== oCompareDeclaration.value
|| oCompareDeclaration.property.match(rProperties)) {

// Add compared rule to diff
if (!oDiffNode) {
oDiffNode = oCompareNode;
oDiffNode.declarations = [];
}
oDiffNode.declarations.push(oCompareDeclaration);
}

}
}
}
}

} else if (oBaseNode.type === "media") {

var aMediaDiffRules = this.diffRules(oBaseNode.rules, oCompareNode.rules);

if (aMediaDiffRules.length > 0) {
oDiffNode = oCompareNode;
oDiffNode.rules = aMediaDiffRules;
}

}

if (oDiffNode) {
aDiffRules.push(oDiffNode);
}

}

// Add all leftover compare nodes to stack
for (; iCompareNode < oCompareRules.length; iCompareNode++) {
this.oStack.stylesheet.rules.push(oCompareRules[iCompareNode]);
}

return aDiffRules;
};

Diffing.prototype.run = function() {
var oBaseRules = this.oBase.stylesheet.rules;
var oCompareRules = this.oCompare.stylesheet.rules;

this.oDiff.stylesheet.rules = this.diffRules(oBaseRules, oCompareRules);

return {
diff: this.oDiff,
stack: this.oStack
};
};


module.exports = function diff(oBase, oCompare) {
return new Diffing(oBase, oCompare).run();
};
81 changes: 81 additions & 0 deletions lib/fileUtils.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
// Copyright 2017 SAP SE.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http: //www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND,
// either express or implied. See the License for the specific
// language governing permissions and limitations under the License.

'use strict';

var fs = require('fs');
var path = require('path');

function statFile(filePath) {
return new Promise(function(resolve, reject) {
fs.stat(filePath, function(err, stat) {
// No rejection here as it is ok if the file was not found
resolve(stat ? { path: filePath, stat: stat } : null);
});
});
}

function statFiles(files) {
return Promise.all(files.map(statFile));
}

function findFile(filePath, rootPaths) {
if (rootPaths && rootPaths.length > 0) {
return statFiles(
rootPaths.map(function(rootPath) {
return path.join(rootPath, filePath);
})
).then(function(results) {
for (var i = 0; i < results.length; i++) {
if (results[i] !== null) {
return results[i];
}
}

// File not found
return null;
});
} else {
return statFile(filePath);
}
}

function readFile(lessInputPath, rootPaths) {
return findFile(lessInputPath, rootPaths).then(function(fileInfo) {
if (!fileInfo) {
return null;
}
return new Promise(function(resolve, reject) {
fs.readFile(fileInfo.path, {
encoding: 'utf8'
}, function(fileErr, content) {
if (fileErr) {
reject(fileErr);
} else {
resolve({
content: content,
path: fileInfo.path,
localPath: lessInputPath,
stats: fileInfo.stats
});
}
});
});
});
}

module.exports.statFile = statFile;
module.exports.statFiles = statFiles;
module.exports.findFile = findFile;
module.exports.readFile = readFile;
Loading