2018-01-18 12:44:09 -05:00
|
|
|
const gulp = require('gulp');
|
|
|
|
|
const through2 = require('through2');
|
|
|
|
|
const gutil = require('gulp-util');
|
|
|
|
|
const autoprefixer = require('gulp-autoprefixer');
|
|
|
|
|
const Buffer = require('buffer').Buffer;
|
|
|
|
|
const fs = require('fs');
|
|
|
|
|
const path = require('path');
|
|
|
|
|
const findModule = require('../config/ngModuleData.js');
|
2014-10-13 18:31:00 -06:00
|
|
|
|
|
|
|
|
exports.humanizeCamelCase = function(str) {
|
2015-06-09 19:46:01 -07:00
|
|
|
switch (str) {
|
|
|
|
|
case 'fabSpeedDial':
|
|
|
|
|
return 'FAB Speed Dial';
|
|
|
|
|
case 'fabToolbar':
|
|
|
|
|
return 'FAB Toolbar';
|
|
|
|
|
default:
|
|
|
|
|
return str.charAt(0).toUpperCase() + str.substring(1).replace(/[A-Z]/g, function($1) {
|
|
|
|
|
return ' ' + $1.toUpperCase();
|
|
|
|
|
});
|
|
|
|
|
}
|
2014-10-13 18:31:00 -06:00
|
|
|
};
|
|
|
|
|
|
2015-01-30 21:41:24 -05:00
|
|
|
/**
|
|
|
|
|
* Copy all the demo assets to the dist directory
|
|
|
|
|
* NOTE: this excludes the modules demo .js,.css, .html files
|
|
|
|
|
*/
|
2015-02-06 12:52:15 -06:00
|
|
|
exports.copyDemoAssets = function(component, srcDir, distDir) {
|
|
|
|
|
gulp.src(srcDir + component + '/demo*/')
|
2019-01-11 17:37:18 -05:00
|
|
|
.pipe(through2.obj(copyAssetsFor));
|
2015-01-30 21:41:24 -05:00
|
|
|
|
2019-01-11 17:37:18 -05:00
|
|
|
function copyAssetsFor(demo, enc, next){
|
2018-01-18 12:44:09 -05:00
|
|
|
const demoID = component + "/" + path.basename(demo.path);
|
|
|
|
|
const demoDir = demo.path + "/**/*";
|
2015-01-30 21:41:24 -05:00
|
|
|
|
2018-01-18 12:44:09 -05:00
|
|
|
const notJS = '!' + demoDir + '.js';
|
|
|
|
|
const notCSS = '!' + demoDir + '.css';
|
|
|
|
|
const notHTML= '!' + demoDir + '.html';
|
2015-01-30 21:41:24 -05:00
|
|
|
|
2015-02-06 12:52:15 -06:00
|
|
|
gulp.src([demoDir, notJS, notCSS, notHTML])
|
|
|
|
|
.pipe(gulp.dest(distDir + demoID));
|
2015-02-08 16:48:00 -06:00
|
|
|
|
|
|
|
|
next();
|
2015-02-06 12:52:15 -06:00
|
|
|
}
|
2015-01-30 21:41:24 -05:00
|
|
|
};
|
|
|
|
|
|
2014-10-13 18:31:00 -06:00
|
|
|
// Gives back a pipe with an array of the parsed data from all of the module's demos
|
2018-01-18 12:44:09 -05:00
|
|
|
// @param moduleName module name to parse
|
2014-10-13 18:31:00 -06:00
|
|
|
// @param fileTasks: tasks to run on the files found in the demo's folder
|
|
|
|
|
// Emits demo objects
|
|
|
|
|
exports.readModuleDemos = function(moduleName, fileTasks) {
|
2018-01-18 12:44:09 -05:00
|
|
|
const name = moduleName.split('.').pop();
|
2014-10-13 18:31:00 -06:00
|
|
|
return gulp.src('src/{components,services}/' + name + '/demo*/')
|
|
|
|
|
.pipe(through2.obj(function(demoFolder, enc, next) {
|
2018-01-18 12:44:09 -05:00
|
|
|
const demoId = name + path.basename(demoFolder.path);
|
2014-10-13 18:31:00 -06:00
|
|
|
|
2018-01-18 12:44:09 -05:00
|
|
|
const demo = {
|
2015-06-16 10:11:21 -07:00
|
|
|
ngModule: '',
|
2014-10-13 18:31:00 -06:00
|
|
|
id: demoId,
|
2015-01-14 18:31:45 -08:00
|
|
|
css:[], html:[], js:[]
|
2014-10-13 18:31:00 -06:00
|
|
|
};
|
|
|
|
|
|
2015-12-11 12:17:03 -06:00
|
|
|
gulp.src(demoFolder.path + '/**/*', { base: path.dirname(demoFolder.path) })
|
2014-10-13 18:31:00 -06:00
|
|
|
.pipe(fileTasks(demoId))
|
|
|
|
|
.pipe(through2.obj(function(file, enc, cb) {
|
|
|
|
|
if (/index.html$/.test(file.path)) {
|
2015-06-16 10:11:21 -07:00
|
|
|
demo.moduleName = moduleName;
|
2014-10-13 18:31:00 -06:00
|
|
|
demo.name = path.basename(demoFolder.path);
|
|
|
|
|
demo.label = exports.humanizeCamelCase(path.basename(demoFolder.path).replace(/^demo/, ''));
|
|
|
|
|
demo.id = demoId;
|
|
|
|
|
demo.index = toDemoObject(file);
|
|
|
|
|
|
|
|
|
|
} else {
|
2018-01-18 12:44:09 -05:00
|
|
|
const fileType = path.extname(file.path).substring(1);
|
|
|
|
|
if (fileType === 'js') {
|
2015-08-25 15:38:01 -05:00
|
|
|
demo.ngModule = demo.ngModule || findModule.any(file.contents.toString());
|
2014-10-13 18:31:00 -06:00
|
|
|
}
|
|
|
|
|
demo[fileType] && demo[fileType].push(toDemoObject(file));
|
|
|
|
|
}
|
|
|
|
|
cb();
|
2018-01-18 12:44:09 -05:00
|
|
|
}, function() {
|
2014-10-13 18:31:00 -06:00
|
|
|
next(null, demo);
|
|
|
|
|
}));
|
|
|
|
|
|
|
|
|
|
function toDemoObject(file) {
|
|
|
|
|
return {
|
|
|
|
|
contents: file.contents.toString(),
|
|
|
|
|
name: path.basename(file.path),
|
|
|
|
|
label: path.basename(file.path),
|
|
|
|
|
fileType: path.extname(file.path).substring(1),
|
2015-05-10 20:45:52 -05:00
|
|
|
outputPath: 'demo-partials/' + name + '/' + path.basename(demoFolder.path) + '/' + path.basename(file.path)
|
2014-10-13 18:31:00 -06:00
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
}));
|
|
|
|
|
};
|
|
|
|
|
|
2018-01-18 12:44:09 -05:00
|
|
|
const pathsForModules = {};
|
2015-02-08 16:48:00 -06:00
|
|
|
|
|
|
|
|
exports.pathsForModule = function(name) {
|
|
|
|
|
return pathsForModules[name] || lookupPath();
|
|
|
|
|
|
|
|
|
|
function lookupPath() {
|
|
|
|
|
gulp.src('src/{services,components,core}/**/*')
|
|
|
|
|
.pipe(through2.obj(function(file, enc, next) {
|
2018-01-18 12:44:09 -05:00
|
|
|
const module = findModule.any(file.contents);
|
|
|
|
|
if (module && module.name === name) {
|
|
|
|
|
const modulePath = file.path.split(path.sep).slice(0, -1).join(path.sep);
|
2015-02-08 16:48:00 -06:00
|
|
|
pathsForModules[name] = modulePath + '/**';
|
|
|
|
|
}
|
|
|
|
|
next();
|
|
|
|
|
}));
|
|
|
|
|
return pathsForModules[name];
|
|
|
|
|
}
|
2018-01-18 12:44:09 -05:00
|
|
|
};
|
2015-02-08 16:48:00 -06:00
|
|
|
|
build: make the build output deterministic and reproducible (#11570)
<!--
Filling out this template is required! Do not delete it when submitting a Pull Request! Without this information, your Pull Request may be auto-closed.
-->
## PR Checklist
Please check that your PR fulfills the following requirements:
- [x] The commit message follows [our guidelines](https://github.com/angular/material/blob/master/.github/CONTRIBUTING.md#-commit-message-format)
- [x] Tests for the changes have been added or this is not a bug fix / enhancement
- [x] Docs have been added, updated, or were not required
## PR Type
What kind of change does this PR introduce?
<!-- Please check the one that applies to this PR using "x". -->
```
[ ] Bugfix
[ ] Enhancement
[ ] Documentation content changes
[ ] Code style update (formatting, local variables)
[ ] Refactoring (no functional changes, no api changes)
[x] Build related changes
[ ] CI related changes
[ ] Infrastructure changes
[ ] Other... Please describe:
```
## What is the current behavior?
Consecutive builds, without any changes, result in bundle content with different ordering. This is even true in a single build where the `dist/angular-material.js` isn't identical to the `dist/docs/angular-material.js`. This makes comparing releases harder and it makes syncing into g3 more time consuming.
- passing globs to `gulp.src` can cause ordering to not be preserved.
this is because `gulp.src` is async
- gulp-concat tries to preserve the order passed into `gulp.src`.
if the globbing is too vague, then there isn't enough info for ordering
<!-- Please describe the current behavior that you are modifying and link to one or more relevant issues. -->
Issue Number:
Fixes #11502
## What is the new behavior?
All of the build output (JS, SCSS, CSS) should be identical if the build is run multiple times without any changes to the code.
## Does this PR introduce a breaking change?
```
[ ] Yes
[x] No
```
<!-- If this PR contains a breaking change, please describe the impact and migration path for existing applications below. -->
<!-- Note that breaking changes are highly unlikely to get merged to master unless the validation is clear and the use case is critical. -->
## Other information
- more statically defining the paths like this isn't ideal but it is acceptable atm since we aren't adding new components.
I looked at sorting the file paths manually or using `gulp-sort` but neither solution was flexible enough to order some of our internal dependencies properly. This approach seems to work fine w/o adding `gulp-order`, but if problems pop up in the future, that is another package that might help.
Note that the root cause of this lies in the intersection of `gulp.src` in Gulp core and `gulp-concat`. This is a well established problem that the Gulp team has mostly decided to punt into user land or third party packages so that they can maintain speed and not need to support crazy levels of customization.
2019-03-13 15:33:00 -05:00
|
|
|
/**
|
|
|
|
|
* @param {string} name module name
|
|
|
|
|
* @returns {*}
|
|
|
|
|
*/
|
2014-10-13 18:31:00 -06:00
|
|
|
exports.filesForModule = function(name) {
|
|
|
|
|
if (pathsForModules[name]) {
|
2014-11-15 13:57:26 -07:00
|
|
|
return srcFiles(pathsForModules[name]);
|
2014-10-13 18:31:00 -06:00
|
|
|
} else {
|
2014-11-15 13:57:26 -07:00
|
|
|
return gulp.src('src/{services,components,core}/**/*')
|
2014-10-13 18:31:00 -06:00
|
|
|
.pipe(through2.obj(function(file, enc, next) {
|
2018-01-18 12:44:09 -05:00
|
|
|
const module = findModule.any(file.contents);
|
|
|
|
|
if (module && (module.name === name)) {
|
|
|
|
|
const modulePath = file.path.split(path.sep).slice(0, -1).join(path.sep);
|
2014-10-13 18:31:00 -06:00
|
|
|
pathsForModules[name] = modulePath + '/**';
|
2018-01-18 12:44:09 -05:00
|
|
|
const self = this;
|
2014-11-15 13:57:26 -07:00
|
|
|
srcFiles(pathsForModules[name]).on('data', function(data) {
|
|
|
|
|
self.push(data);
|
|
|
|
|
});
|
2014-10-13 18:31:00 -06:00
|
|
|
}
|
|
|
|
|
next();
|
|
|
|
|
}));
|
|
|
|
|
}
|
2014-11-15 13:57:26 -07:00
|
|
|
|
|
|
|
|
function srcFiles(path) {
|
|
|
|
|
return gulp.src(path)
|
|
|
|
|
.pipe(through2.obj(function(file, enc, next) {
|
|
|
|
|
if (file.stat.isFile()) next(null, file);
|
|
|
|
|
else next();
|
|
|
|
|
}));
|
|
|
|
|
}
|
2014-10-13 18:31:00 -06:00
|
|
|
};
|
|
|
|
|
|
2015-05-27 18:16:11 -04:00
|
|
|
exports.appendToFile = function(filePath) {
|
2018-01-18 12:44:09 -05:00
|
|
|
let bufferedContents;
|
2015-05-27 18:16:11 -04:00
|
|
|
return through2.obj(function(file, enc, next) {
|
|
|
|
|
bufferedContents = file.contents.toString('utf8') + '\n';
|
|
|
|
|
next();
|
|
|
|
|
}, function(done) {
|
2018-01-18 12:44:09 -05:00
|
|
|
const existing = fs.readFileSync(filePath, 'utf8');
|
2015-05-27 18:16:11 -04:00
|
|
|
bufferedContents = existing + '\n' + bufferedContents;
|
2018-01-18 12:44:09 -05:00
|
|
|
const outputFile = new gutil.File({
|
2015-05-27 18:16:11 -04:00
|
|
|
cwd: process.cwd(),
|
|
|
|
|
base: path.dirname(filePath),
|
|
|
|
|
path: filePath,
|
2018-01-18 12:44:09 -05:00
|
|
|
contents: Buffer.from(bufferedContents)
|
2015-05-27 18:16:11 -04:00
|
|
|
});
|
|
|
|
|
this.push(outputFile);
|
|
|
|
|
done();
|
|
|
|
|
});
|
|
|
|
|
};
|
|
|
|
|
|
2014-10-13 18:31:00 -06:00
|
|
|
exports.buildNgMaterialDefinition = function() {
|
2018-01-18 12:44:09 -05:00
|
|
|
let srcBuffer = [];
|
|
|
|
|
const modulesSeen = [];
|
2014-10-13 18:31:00 -06:00
|
|
|
return through2.obj(function(file, enc, next) {
|
2018-01-18 12:44:09 -05:00
|
|
|
const module = findModule.material(file.contents);
|
2015-08-25 15:38:01 -05:00
|
|
|
if (module) modulesSeen.push(module.name);
|
|
|
|
|
srcBuffer.push(file);
|
2014-10-13 18:31:00 -06:00
|
|
|
next();
|
|
|
|
|
}, function(done) {
|
2018-01-18 12:44:09 -05:00
|
|
|
const self = this;
|
|
|
|
|
const requiredLibs = ['ng', 'ngAnimate', 'ngAria'];
|
|
|
|
|
const dependencies = JSON.stringify(requiredLibs.concat(modulesSeen));
|
|
|
|
|
const ngMaterialModule = "angular.module('ngMaterial', " + dependencies + ');';
|
|
|
|
|
const angularFile = new gutil.File({
|
2014-10-13 18:31:00 -06:00
|
|
|
base: process.cwd(),
|
|
|
|
|
path: process.cwd() + '/ngMaterial.js',
|
2018-01-18 12:44:09 -05:00
|
|
|
contents: Buffer.from(ngMaterialModule)
|
2014-10-13 18:31:00 -06:00
|
|
|
});
|
2015-08-25 15:38:01 -05:00
|
|
|
|
|
|
|
|
// Elevate ngMaterial module registration to first in queue
|
|
|
|
|
self.push(angularFile);
|
|
|
|
|
|
|
|
|
|
srcBuffer.forEach(function(file) {
|
2014-10-13 18:31:00 -06:00
|
|
|
self.push(file);
|
|
|
|
|
});
|
2015-08-25 15:38:01 -05:00
|
|
|
|
|
|
|
|
srcBuffer = [];
|
2014-10-13 18:31:00 -06:00
|
|
|
done();
|
|
|
|
|
});
|
|
|
|
|
};
|
|
|
|
|
|
2014-11-10 13:53:08 -05:00
|
|
|
function moduleNameToClosureName(name) {
|
2016-07-14 12:33:23 -07:00
|
|
|
// For Closure, all modules start with "ngmaterial". We specifically don't use `ng.`
|
|
|
|
|
// because it conflicts with other packages under `ng.`.
|
|
|
|
|
return 'ng' + name;
|
2014-11-10 13:53:08 -05:00
|
|
|
}
|
2015-05-10 20:45:52 -05:00
|
|
|
exports.addJsWrapper = function(enforce) {
|
2015-04-22 10:35:47 -07:00
|
|
|
return through2.obj(function(file, enc, next) {
|
2018-01-18 12:44:09 -05:00
|
|
|
const module = findModule.any(file.contents);
|
2015-08-25 15:38:01 -05:00
|
|
|
if (!!enforce || module) {
|
2018-01-18 12:44:09 -05:00
|
|
|
file.contents = Buffer.from([
|
|
|
|
|
enforce ? '(function(){' : '(function( window, angular, undefined ){',
|
2015-05-10 20:45:52 -05:00
|
|
|
'"use strict";\n',
|
2015-04-22 10:35:47 -07:00
|
|
|
file.contents.toString(),
|
2018-01-18 12:44:09 -05:00
|
|
|
enforce ? '})();' : '})(window, window.angular);'
|
2015-04-22 10:35:47 -07:00
|
|
|
].join('\n'));
|
|
|
|
|
}
|
|
|
|
|
this.push(file);
|
|
|
|
|
next();
|
|
|
|
|
});
|
|
|
|
|
};
|
2014-11-10 13:53:08 -05:00
|
|
|
exports.addClosurePrefixes = function() {
|
|
|
|
|
return through2.obj(function(file, enc, next) {
|
2018-01-18 12:44:09 -05:00
|
|
|
const module = findModule.any(file.contents);
|
2015-08-25 15:38:01 -05:00
|
|
|
if (module) {
|
2018-01-18 12:44:09 -05:00
|
|
|
const closureModuleName = moduleNameToClosureName(module.name);
|
|
|
|
|
const requires = (module.dependencies || []).sort().map(function(dep) {
|
|
|
|
|
if (dep.indexOf(module.name) === 0 || /material\..+/g.test(dep) === false) return '';
|
2016-01-17 11:34:37 +01:00
|
|
|
return 'goog.require(\'' + moduleNameToClosureName(dep) + '\');';
|
2014-11-10 13:53:08 -05:00
|
|
|
}).join('\n');
|
|
|
|
|
|
2018-01-18 12:44:09 -05:00
|
|
|
file.contents = Buffer.from([
|
2015-04-22 10:20:50 -07:00
|
|
|
'goog.provide(\'' + closureModuleName + '\');',
|
|
|
|
|
requires,
|
|
|
|
|
file.contents.toString(),
|
2015-08-25 15:38:01 -05:00
|
|
|
closureModuleName + ' = angular.module("' + module.name + '");'
|
2015-04-22 10:20:50 -07:00
|
|
|
].join('\n'));
|
2014-11-10 13:53:08 -05:00
|
|
|
}
|
|
|
|
|
this.push(file);
|
|
|
|
|
next();
|
|
|
|
|
});
|
|
|
|
|
};
|
|
|
|
|
|
2014-10-13 18:31:00 -06:00
|
|
|
exports.buildModuleBower = function(name, version) {
|
|
|
|
|
return through2.obj(function(file, enc, next) {
|
|
|
|
|
this.push(file);
|
2018-01-18 12:44:09 -05:00
|
|
|
const module = findModule.any(file.contents);
|
2015-08-25 15:38:01 -05:00
|
|
|
if (module) {
|
2018-01-18 12:44:09 -05:00
|
|
|
const bowerDeps = {};
|
2015-08-25 15:38:01 -05:00
|
|
|
(module.dependencies || []).forEach(function(dep) {
|
2018-01-18 12:44:09 -05:00
|
|
|
const convertedName = 'angular-material-' + dep.split('.').pop();
|
2014-10-13 18:31:00 -06:00
|
|
|
bowerDeps[convertedName] = version;
|
|
|
|
|
});
|
2018-01-18 12:44:09 -05:00
|
|
|
const bowerContents = JSON.stringify({
|
2014-10-13 18:31:00 -06:00
|
|
|
name: 'angular-material-' + name,
|
|
|
|
|
version: version,
|
|
|
|
|
dependencies: bowerDeps
|
2014-11-10 13:53:08 -05:00
|
|
|
}, null, 2);
|
2018-01-18 12:44:09 -05:00
|
|
|
const bowerFile = new gutil.File({
|
2014-10-13 18:31:00 -06:00
|
|
|
base: file.base,
|
|
|
|
|
path: file.base + '/bower.json',
|
2018-01-18 12:44:09 -05:00
|
|
|
contents: Buffer.from(bowerContents)
|
2014-10-13 18:31:00 -06:00
|
|
|
});
|
|
|
|
|
this.push(bowerFile);
|
|
|
|
|
}
|
|
|
|
|
next();
|
|
|
|
|
});
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
exports.hoistScssVariables = function() {
|
|
|
|
|
return through2.obj(function(file, enc, next) {
|
2018-01-18 12:44:09 -05:00
|
|
|
const contents = file.contents.toString().split('\n');
|
|
|
|
|
let lastVariableLine = -1;
|
2014-10-13 18:31:00 -06:00
|
|
|
|
2018-01-18 12:44:09 -05:00
|
|
|
let openCount = 0;
|
|
|
|
|
let closeCount = 0;
|
|
|
|
|
let openBlock = false;
|
2014-10-14 19:36:43 -04:00
|
|
|
|
2019-01-11 18:42:44 -05:00
|
|
|
for (let currentLine = 0; currentLine < contents.length; ++currentLine) {
|
2018-01-18 12:44:09 -05:00
|
|
|
const line = contents[currentLine];
|
2014-10-14 19:36:43 -04:00
|
|
|
|
2014-12-05 15:34:19 -07:00
|
|
|
if (openBlock || /^\s*\$/.test(line) && !/^\s+/.test(line)) {
|
2014-10-14 19:36:43 -04:00
|
|
|
openCount += (line.match(/\(/g) || []).length;
|
|
|
|
|
closeCount += (line.match(/\)/g) || []).length;
|
2018-01-18 12:44:09 -05:00
|
|
|
openBlock = openCount !== closeCount;
|
|
|
|
|
const variable = contents.splice(currentLine, 1)[0];
|
2014-10-14 19:36:43 -04:00
|
|
|
contents.splice(++lastVariableLine, 0, variable);
|
2014-10-13 18:31:00 -06:00
|
|
|
}
|
|
|
|
|
}
|
2018-01-18 12:44:09 -05:00
|
|
|
file.contents = Buffer.from(contents.join('\n'));
|
2014-10-13 18:31:00 -06:00
|
|
|
this.push(file);
|
|
|
|
|
next();
|
|
|
|
|
});
|
|
|
|
|
};
|
2014-12-05 15:34:19 -07:00
|
|
|
|
|
|
|
|
exports.cssToNgConstant = function(ngModule, factoryName) {
|
|
|
|
|
return through2.obj(function(file, enc, next) {
|
|
|
|
|
|
2018-01-18 12:44:09 -05:00
|
|
|
const template = '(function(){ \nangular.module("%1").constant("%2", "%3"); \n})();\n\n';
|
|
|
|
|
const output = file.contents.toString().replace(/\n/g, '').replace(/"/g,'\\"');
|
2014-12-05 15:34:19 -07:00
|
|
|
|
2018-01-18 12:44:09 -05:00
|
|
|
const jsFile = new gutil.File({
|
2014-12-05 15:34:19 -07:00
|
|
|
base: file.base,
|
|
|
|
|
path: file.path.replace('css', 'js'),
|
2018-01-18 12:44:09 -05:00
|
|
|
contents: Buffer.from(
|
2014-12-05 15:34:19 -07:00
|
|
|
template.replace('%1', ngModule)
|
|
|
|
|
.replace('%2', factoryName)
|
|
|
|
|
.replace('%3', output)
|
|
|
|
|
)
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
this.push(jsFile);
|
|
|
|
|
next();
|
|
|
|
|
});
|
|
|
|
|
};
|
2016-10-10 20:13:25 +02:00
|
|
|
|
2018-06-29 16:13:45 -04:00
|
|
|
/**
|
|
|
|
|
* Use the configuration in the "browserslist" field of the package.json as recommended
|
|
|
|
|
* by the autoprefixer docs.
|
|
|
|
|
* @returns {NodeJS.ReadWriteStream | *}
|
|
|
|
|
*/
|
2016-10-10 20:13:25 +02:00
|
|
|
exports.autoprefix = function() {
|
2018-06-29 16:13:45 -04:00
|
|
|
return autoprefixer();
|
2016-10-10 20:13:25 +02:00
|
|
|
};
|