less 4.5.1 → 4.6.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +70 -5
- package/bin/lessc +37 -23
- package/dist/less.js +14501 -11745
- package/dist/less.min.js +4 -4
- package/dist/less.min.js.map +1 -1
- package/index.cjs +41 -0
- package/lib/less/constants.js +5 -7
- package/lib/less/contexts.js +71 -55
- package/lib/less/data/colors.js +150 -153
- package/lib/less/data/index.js +4 -7
- package/lib/less/data/unit-conversions.js +2 -5
- package/lib/less/default-options.js +36 -26
- package/lib/less/deprecation.js +67 -0
- package/lib/less/environment/abstract-file-manager.js +62 -50
- package/lib/less/environment/abstract-plugin-loader.js +69 -46
- package/lib/less/environment/environment-api.ts +21 -0
- package/lib/less/environment/environment.js +34 -30
- package/lib/less/environment/file-manager-api.ts +77 -0
- package/lib/less/functions/boolean.js +13 -13
- package/lib/less/functions/color-blending.js +39 -31
- package/lib/less/functions/color.js +153 -137
- package/lib/less/functions/data-uri.js +73 -64
- package/lib/less/functions/default.js +9 -11
- package/lib/less/functions/function-caller.js +35 -34
- package/lib/less/functions/function-registry.js +16 -17
- package/lib/less/functions/index.js +33 -33
- package/lib/less/functions/list.js +88 -75
- package/lib/less/functions/math-helper.js +8 -11
- package/lib/less/functions/math.js +21 -21
- package/lib/less/functions/number.js +42 -53
- package/lib/less/functions/string.js +18 -22
- package/lib/less/functions/style.js +21 -20
- package/lib/less/functions/svg.js +83 -82
- package/lib/less/functions/types.js +28 -32
- package/lib/less/import-manager.js +73 -64
- package/lib/less/index.js +65 -65
- package/lib/less/less-error.js +63 -45
- package/lib/less/logger.js +11 -14
- package/lib/less/parse-tree.js +52 -53
- package/lib/less/parse.js +44 -44
- package/lib/less/parser/parser-input.js +151 -113
- package/lib/less/parser/parser.js +864 -658
- package/lib/less/plugin-manager.js +57 -46
- package/lib/less/render.js +17 -23
- package/lib/less/source-map-builder.js +52 -43
- package/lib/less/source-map-output.js +56 -43
- package/lib/less/transform-tree.js +40 -30
- package/lib/less/tree/anonymous.js +52 -28
- package/lib/less/tree/assignment.js +44 -27
- package/lib/less/tree/atrule-syntax.js +4 -6
- package/lib/less/tree/atrule.js +247 -140
- package/lib/less/tree/attribute.js +55 -24
- package/lib/less/tree/call.js +70 -42
- package/lib/less/tree/color.js +194 -145
- package/lib/less/tree/combinator.js +31 -21
- package/lib/less/tree/comment.js +44 -23
- package/lib/less/tree/condition.js +61 -36
- package/lib/less/tree/container.js +90 -34
- package/lib/less/tree/debug-info.js +28 -15
- package/lib/less/tree/declaration.js +97 -52
- package/lib/less/tree/detached-ruleset.js +42 -24
- package/lib/less/tree/dimension.js +127 -71
- package/lib/less/tree/element.js +82 -52
- package/lib/less/tree/expression.js +75 -50
- package/lib/less/tree/extend.js +71 -42
- package/lib/less/tree/import.js +184 -110
- package/lib/less/tree/index.js +52 -81
- package/lib/less/tree/javascript.js +43 -32
- package/lib/less/tree/js-eval-node.js +46 -32
- package/lib/less/tree/keyword.js +25 -17
- package/lib/less/tree/media.js +77 -35
- package/lib/less/tree/merge-rules.js +47 -0
- package/lib/less/tree/mixin-call.js +181 -98
- package/lib/less/tree/mixin-definition.js +211 -117
- package/lib/less/tree/namespace-value.js +63 -44
- package/lib/less/tree/negative.js +34 -21
- package/lib/less/tree/nested-at-rule.js +149 -66
- package/lib/less/tree/node.js +225 -90
- package/lib/less/tree/operation.js +59 -34
- package/lib/less/tree/paren.js +34 -17
- package/lib/less/tree/property.js +66 -35
- package/lib/less/tree/query-in-parens.js +52 -60
- package/lib/less/tree/quoted.js +93 -44
- package/lib/less/tree/ruleset.js +632 -318
- package/lib/less/tree/selector.js +151 -77
- package/lib/less/tree/unicode-descriptor.js +14 -12
- package/lib/less/tree/unit.js +111 -63
- package/lib/less/tree/url.js +62 -38
- package/lib/less/tree/value.js +50 -34
- package/lib/less/tree/variable-call.js +47 -30
- package/lib/less/tree/variable.js +53 -31
- package/lib/less/utils.js +44 -47
- package/lib/less/visitors/extend-visitor.js +233 -172
- package/lib/less/visitors/import-sequencer.js +24 -24
- package/lib/less/visitors/import-visitor.js +62 -52
- package/lib/less/visitors/index.js +14 -17
- package/lib/less/visitors/join-selector-visitor.js +40 -37
- package/lib/less/visitors/set-tree-visibility-visitor.js +32 -18
- package/lib/less/visitors/to-css-visitor.js +135 -128
- package/lib/less/visitors/visitor.js +60 -45
- package/lib/less-browser/add-default-options.js +23 -13
- package/lib/less-browser/bootstrap.js +23 -17
- package/lib/less-browser/browser.js +24 -21
- package/lib/less-browser/cache.js +17 -19
- package/lib/less-browser/error-reporting.js +52 -44
- package/lib/less-browser/file-manager.js +52 -42
- package/lib/less-browser/image-size.js +12 -12
- package/lib/less-browser/index.js +131 -105
- package/lib/less-browser/log-listener.js +30 -31
- package/lib/less-browser/plugin-loader.js +10 -10
- package/lib/less-browser/utils.js +19 -21
- package/lib/less-node/environment.js +32 -8
- package/lib/less-node/file-manager.js +62 -45
- package/lib/less-node/fs.js +11 -8
- package/lib/less-node/image-size.js +40 -31
- package/lib/less-node/index.js +28 -19
- package/lib/less-node/lessc-helper.js +20 -21
- package/lib/less-node/plugin-loader.js +35 -27
- package/lib/less-node/url-file-manager.js +33 -31
- package/package.json +29 -18
- package/.eslintignore +0 -6
- package/.eslintrc.js +0 -63
- package/Gruntfile.js +0 -403
- package/bower.json +0 -22
- package/lib/less/constants.js.map +0 -1
- package/lib/less/contexts.js.map +0 -1
- package/lib/less/data/colors.js.map +0 -1
- package/lib/less/data/index.js.map +0 -1
- package/lib/less/data/unit-conversions.js.map +0 -1
- package/lib/less/default-options.js.map +0 -1
- package/lib/less/environment/abstract-file-manager.js.map +0 -1
- package/lib/less/environment/abstract-plugin-loader.js.map +0 -1
- package/lib/less/environment/environment-api.js +0 -3
- package/lib/less/environment/environment-api.js.map +0 -1
- package/lib/less/environment/environment.js.map +0 -1
- package/lib/less/environment/file-manager-api.js +0 -3
- package/lib/less/environment/file-manager-api.js.map +0 -1
- package/lib/less/functions/boolean.js.map +0 -1
- package/lib/less/functions/color-blending.js.map +0 -1
- package/lib/less/functions/color.js.map +0 -1
- package/lib/less/functions/data-uri.js.map +0 -1
- package/lib/less/functions/default.js.map +0 -1
- package/lib/less/functions/function-caller.js.map +0 -1
- package/lib/less/functions/function-registry.js.map +0 -1
- package/lib/less/functions/index.js.map +0 -1
- package/lib/less/functions/list.js.map +0 -1
- package/lib/less/functions/math-helper.js.map +0 -1
- package/lib/less/functions/math.js.map +0 -1
- package/lib/less/functions/number.js.map +0 -1
- package/lib/less/functions/string.js.map +0 -1
- package/lib/less/functions/style.js.map +0 -1
- package/lib/less/functions/svg.js.map +0 -1
- package/lib/less/functions/types.js.map +0 -1
- package/lib/less/import-manager.js.map +0 -1
- package/lib/less/index.js.map +0 -1
- package/lib/less/less-error.js.map +0 -1
- package/lib/less/logger.js.map +0 -1
- package/lib/less/parse-tree.js.map +0 -1
- package/lib/less/parse.js.map +0 -1
- package/lib/less/parser/parser-input.js.map +0 -1
- package/lib/less/parser/parser.js.map +0 -1
- package/lib/less/plugin-manager.js.map +0 -1
- package/lib/less/render.js.map +0 -1
- package/lib/less/source-map-builder.js.map +0 -1
- package/lib/less/source-map-output.js.map +0 -1
- package/lib/less/transform-tree.js.map +0 -1
- package/lib/less/tree/anonymous.js.map +0 -1
- package/lib/less/tree/assignment.js.map +0 -1
- package/lib/less/tree/atrule-syntax.js.map +0 -1
- package/lib/less/tree/atrule.js.map +0 -1
- package/lib/less/tree/attribute.js.map +0 -1
- package/lib/less/tree/call.js.map +0 -1
- package/lib/less/tree/color.js.map +0 -1
- package/lib/less/tree/combinator.js.map +0 -1
- package/lib/less/tree/comment.js.map +0 -1
- package/lib/less/tree/condition.js.map +0 -1
- package/lib/less/tree/container.js.map +0 -1
- package/lib/less/tree/debug-info.js.map +0 -1
- package/lib/less/tree/declaration.js.map +0 -1
- package/lib/less/tree/detached-ruleset.js.map +0 -1
- package/lib/less/tree/dimension.js.map +0 -1
- package/lib/less/tree/element.js.map +0 -1
- package/lib/less/tree/expression.js.map +0 -1
- package/lib/less/tree/extend.js.map +0 -1
- package/lib/less/tree/import.js.map +0 -1
- package/lib/less/tree/index.js.map +0 -1
- package/lib/less/tree/javascript.js.map +0 -1
- package/lib/less/tree/js-eval-node.js.map +0 -1
- package/lib/less/tree/keyword.js.map +0 -1
- package/lib/less/tree/media.js.map +0 -1
- package/lib/less/tree/mixin-call.js.map +0 -1
- package/lib/less/tree/mixin-definition.js.map +0 -1
- package/lib/less/tree/namespace-value.js.map +0 -1
- package/lib/less/tree/negative.js.map +0 -1
- package/lib/less/tree/nested-at-rule.js.map +0 -1
- package/lib/less/tree/node.js.map +0 -1
- package/lib/less/tree/operation.js.map +0 -1
- package/lib/less/tree/paren.js.map +0 -1
- package/lib/less/tree/property.js.map +0 -1
- package/lib/less/tree/query-in-parens.js.map +0 -1
- package/lib/less/tree/quoted.js.map +0 -1
- package/lib/less/tree/ruleset.js.map +0 -1
- package/lib/less/tree/selector.js.map +0 -1
- package/lib/less/tree/unicode-descriptor.js.map +0 -1
- package/lib/less/tree/unit.js.map +0 -1
- package/lib/less/tree/url.js.map +0 -1
- package/lib/less/tree/value.js.map +0 -1
- package/lib/less/tree/variable-call.js.map +0 -1
- package/lib/less/tree/variable.js.map +0 -1
- package/lib/less/utils.js.map +0 -1
- package/lib/less/visitors/extend-visitor.js.map +0 -1
- package/lib/less/visitors/import-sequencer.js.map +0 -1
- package/lib/less/visitors/import-visitor.js.map +0 -1
- package/lib/less/visitors/index.js.map +0 -1
- package/lib/less/visitors/join-selector-visitor.js.map +0 -1
- package/lib/less/visitors/set-tree-visibility-visitor.js.map +0 -1
- package/lib/less/visitors/to-css-visitor.js.map +0 -1
- package/lib/less/visitors/visitor.js.map +0 -1
- package/lib/less-browser/add-default-options.js.map +0 -1
- package/lib/less-browser/bootstrap.js.map +0 -1
- package/lib/less-browser/browser.js.map +0 -1
- package/lib/less-browser/cache.js.map +0 -1
- package/lib/less-browser/error-reporting.js.map +0 -1
- package/lib/less-browser/file-manager.js.map +0 -1
- package/lib/less-browser/image-size.js.map +0 -1
- package/lib/less-browser/index.js.map +0 -1
- package/lib/less-browser/log-listener.js.map +0 -1
- package/lib/less-browser/plugin-loader.js.map +0 -1
- package/lib/less-browser/utils.js.map +0 -1
- package/lib/less-node/environment.js.map +0 -1
- package/lib/less-node/file-manager.js.map +0 -1
- package/lib/less-node/fs.js.map +0 -1
- package/lib/less-node/image-size.js.map +0 -1
- package/lib/less-node/index.js.map +0 -1
- package/lib/less-node/lessc-helper.js.map +0 -1
- package/lib/less-node/plugin-loader.js.map +0 -1
- package/lib/less-node/url-file-manager.js.map +0 -1
- package/scripts/coverage-lines.js +0 -207
- package/scripts/coverage-report.js +0 -158
- package/scripts/postinstall.js +0 -61
- package/test/.eslintrc.json +0 -9
- package/test/README.md +0 -3
- package/test/browser/common.js +0 -251
- package/test/browser/css/global-vars/simple.css +0 -3
- package/test/browser/css/modify-vars/simple.css +0 -8
- package/test/browser/css/plugin/plugin.css +0 -3
- package/test/browser/css/postProcessor/postProcessor.css +0 -4
- package/test/browser/css/relative-urls/urls.css +0 -36
- package/test/browser/css/rewrite-urls/urls.css +0 -36
- package/test/browser/css/rootpath/urls.css +0 -35
- package/test/browser/css/rootpath-relative/urls.css +0 -35
- package/test/browser/css/rootpath-rewrite-urls/urls.css +0 -35
- package/test/browser/css/urls.css +0 -54
- package/test/browser/generator/benchmark.config.js +0 -50
- package/test/browser/generator/generate.js +0 -78
- package/test/browser/generator/runner.config.js +0 -184
- package/test/browser/generator/runner.js +0 -2
- package/test/browser/generator/template.js +0 -104
- package/test/browser/generator/utils.js +0 -14
- package/test/browser/less/console-errors/test-error.less +0 -3
- package/test/browser/less/console-errors/test-error.txt +0 -2
- package/test/browser/less/errors/image-height-error.less +0 -3
- package/test/browser/less/errors/image-height-error.txt +0 -4
- package/test/browser/less/errors/image-size-error.less +0 -3
- package/test/browser/less/errors/image-size-error.txt +0 -4
- package/test/browser/less/errors/image-width-error.less +0 -3
- package/test/browser/less/errors/image-width-error.txt +0 -4
- package/test/browser/less/global-vars/simple.less +0 -3
- package/test/browser/less/imports/urls.less +0 -4
- package/test/browser/less/imports/urls2.less +0 -4
- package/test/browser/less/modify-vars/imports/simple2.less +0 -4
- package/test/browser/less/modify-vars/simple.less +0 -8
- package/test/browser/less/nested-gradient-with-svg-gradient/mixin-consumer.less +0 -5
- package/test/browser/less/nested-gradient-with-svg-gradient/svg-gradient-mixin.less +0 -15
- package/test/browser/less/plugin/plugin.js +0 -4
- package/test/browser/less/plugin/plugin.less +0 -4
- package/test/browser/less/postProcessor/postProcessor.less +0 -4
- package/test/browser/less/relative-urls/urls.less +0 -34
- package/test/browser/less/rewrite-urls/urls.less +0 -34
- package/test/browser/less/rootpath/urls.less +0 -33
- package/test/browser/less/rootpath-relative/urls.less +0 -33
- package/test/browser/less/rootpath-rewrite-urls/urls.less +0 -33
- package/test/browser/less/urls.less +0 -62
- package/test/browser/runner-VisitorPlugin-options.js +0 -3
- package/test/browser/runner-VisitorPlugin.js +0 -3
- package/test/browser/runner-browser-options.js +0 -51
- package/test/browser/runner-browser-spec.js +0 -12
- package/test/browser/runner-console-errors.js +0 -5
- package/test/browser/runner-errors-options.js +0 -6
- package/test/browser/runner-errors-spec.js +0 -3
- package/test/browser/runner-filemanagerPlugin-options.js +0 -5
- package/test/browser/runner-filemanagerPlugin.js +0 -3
- package/test/browser/runner-global-vars-options.js +0 -7
- package/test/browser/runner-global-vars-spec.js +0 -3
- package/test/browser/runner-main-options.js +0 -17
- package/test/browser/runner-main-spec.js +0 -7
- package/test/browser/runner-modify-vars-options.js +0 -5
- package/test/browser/runner-modify-vars-spec.js +0 -33
- package/test/browser/runner-no-js-errors-options.js +0 -4
- package/test/browser/runner-no-js-errors-spec.js +0 -3
- package/test/browser/runner-postProcessorPlugin-options.js +0 -3
- package/test/browser/runner-postProcessorPlugin.js +0 -3
- package/test/browser/runner-preProcessorPlugin-options.js +0 -3
- package/test/browser/runner-preProcessorPlugin.js +0 -3
- package/test/browser/runner-production-options.js +0 -3
- package/test/browser/runner-production-spec.js +0 -5
- package/test/browser/runner-relative-urls-options.js +0 -3
- package/test/browser/runner-relative-urls-spec.js +0 -3
- package/test/browser/runner-rewrite-urls-options.js +0 -3
- package/test/browser/runner-rewrite-urls-spec.js +0 -3
- package/test/browser/runner-rootpath-options.js +0 -3
- package/test/browser/runner-rootpath-relative-options.js +0 -4
- package/test/browser/runner-rootpath-relative-spec.js +0 -3
- package/test/browser/runner-rootpath-rewrite-urls-options.js +0 -4
- package/test/browser/runner-rootpath-rewrite-urls-spec.js +0 -3
- package/test/browser/runner-rootpath-spec.js +0 -3
- package/test/browser/runner-strict-units-options.js +0 -5
- package/test/browser/runner-strict-units-spec.js +0 -3
- package/test/index.js +0 -307
- package/test/less-test.js +0 -984
- package/test/mocha-playwright/runner.js +0 -210
- package/test/modify-vars.js +0 -28
- package/test/plugins/filemanager/index.js +0 -19
- package/test/plugins/postprocess/index.js +0 -14
- package/test/plugins/preprocess/index.js +0 -19
- package/test/plugins/visitor/index.js +0 -24
- package/test/sourcemaps/basic.json +0 -1
- package/test/sourcemaps/comprehensive.json +0 -1
- package/test/sourcemaps/custom-props.json +0 -1
- package/test/sourcemaps/index.html +0 -17
- package/test/sourcemaps/sourcemaps-basepath.json +0 -1
- package/test/sourcemaps/sourcemaps-include-source.json +0 -1
- package/test/sourcemaps/sourcemaps-rootpath.json +0 -1
- package/test/sourcemaps/sourcemaps-url.json +0 -1
- package/test/sourcemaps-disable-annotation/basic.json +0 -1
- package/test/sourcemaps-variable-selector/basic.json +0 -1
- package/test/test-es6.ts +0 -17
- package/test.less +0 -1
- package/tsconfig.build.json +0 -7
- package/tsconfig.json +0 -21
package/test/less-test.js
DELETED
|
@@ -1,984 +0,0 @@
|
|
|
1
|
-
/* jshint latedef: nofunc */
|
|
2
|
-
var semver = require('semver');
|
|
3
|
-
var logger = require('../lib/less/logger').default;
|
|
4
|
-
var { cosmiconfigSync } = require('cosmiconfig');
|
|
5
|
-
var glob = require('glob');
|
|
6
|
-
|
|
7
|
-
var isVerbose = process.env.npm_config_loglevel !== 'concise';
|
|
8
|
-
logger.addListener({
|
|
9
|
-
info(msg) {
|
|
10
|
-
if (isVerbose) {
|
|
11
|
-
process.stdout.write(msg + '\n');
|
|
12
|
-
}
|
|
13
|
-
},
|
|
14
|
-
warn(msg) {
|
|
15
|
-
process.stdout.write(msg + '\n');
|
|
16
|
-
},
|
|
17
|
-
error(msg) {
|
|
18
|
-
process.stdout.write(msg + '\n');
|
|
19
|
-
}
|
|
20
|
-
});
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
module.exports = function(testFilter) {
|
|
24
|
-
var path = require('path'),
|
|
25
|
-
fs = require('fs'),
|
|
26
|
-
clone = require('copy-anything').copy;
|
|
27
|
-
|
|
28
|
-
var less = require('../');
|
|
29
|
-
|
|
30
|
-
var stylize = require('../lib/less-node/lessc-helper').stylize;
|
|
31
|
-
|
|
32
|
-
var globals = Object.keys(global);
|
|
33
|
-
|
|
34
|
-
var oneTestOnly = testFilter || process.argv[2],
|
|
35
|
-
isFinished = false;
|
|
36
|
-
|
|
37
|
-
var testFolder = path.dirname(require.resolve('@less/test-data'));
|
|
38
|
-
var lessFolder = testFolder;
|
|
39
|
-
|
|
40
|
-
// Define String.prototype.endsWith if it doesn't exist (in older versions of node)
|
|
41
|
-
// This is required by the testSourceMap function below
|
|
42
|
-
if (typeof String.prototype.endsWith !== 'function') {
|
|
43
|
-
String.prototype.endsWith = function (str) {
|
|
44
|
-
return this.slice(-str.length) === str;
|
|
45
|
-
}
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
var queueList = [],
|
|
49
|
-
queueRunning = false;
|
|
50
|
-
function queue(func) {
|
|
51
|
-
if (queueRunning) {
|
|
52
|
-
// console.log("adding to queue");
|
|
53
|
-
queueList.push(func);
|
|
54
|
-
} else {
|
|
55
|
-
// console.log("first in queue - starting");
|
|
56
|
-
queueRunning = true;
|
|
57
|
-
func();
|
|
58
|
-
}
|
|
59
|
-
}
|
|
60
|
-
function release() {
|
|
61
|
-
if (queueList.length) {
|
|
62
|
-
// console.log("running next in queue");
|
|
63
|
-
var func = queueList.shift();
|
|
64
|
-
setTimeout(func, 0);
|
|
65
|
-
} else {
|
|
66
|
-
// console.log("stopping queue");
|
|
67
|
-
queueRunning = false;
|
|
68
|
-
}
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
var totalTests = 0,
|
|
72
|
-
failedTests = 0,
|
|
73
|
-
passedTests = 0,
|
|
74
|
-
finishTimer = setInterval(endTest, 500);
|
|
75
|
-
|
|
76
|
-
less.functions.functionRegistry.addMultiple({
|
|
77
|
-
add: function (a, b) {
|
|
78
|
-
return new(less.tree.Dimension)(a.value + b.value);
|
|
79
|
-
},
|
|
80
|
-
increment: function (a) {
|
|
81
|
-
return new(less.tree.Dimension)(a.value + 1);
|
|
82
|
-
},
|
|
83
|
-
_color: function (str) {
|
|
84
|
-
if (str.value === 'evil red') { return new(less.tree.Color)('600'); }
|
|
85
|
-
}
|
|
86
|
-
});
|
|
87
|
-
|
|
88
|
-
function validateSourcemapMappings(sourcemap, lessFile, compiledCSS) {
|
|
89
|
-
// Validate sourcemap mappings using SourceMapConsumer
|
|
90
|
-
var SourceMapConsumer = require('source-map').SourceMapConsumer;
|
|
91
|
-
// sourcemap can be either a string or already parsed object
|
|
92
|
-
var sourceMapObj = typeof sourcemap === 'string' ? JSON.parse(sourcemap) : sourcemap;
|
|
93
|
-
var consumer = new SourceMapConsumer(sourceMapObj);
|
|
94
|
-
|
|
95
|
-
// Read the LESS source file
|
|
96
|
-
var lessSource = fs.readFileSync(lessFile, 'utf8');
|
|
97
|
-
var lessLines = lessSource.split('\n');
|
|
98
|
-
|
|
99
|
-
// Use the compiled CSS (remove sourcemap annotation for validation)
|
|
100
|
-
var cssSource = compiledCSS.replace(/\/\*# sourceMappingURL=.*\*\/\s*$/, '').trim();
|
|
101
|
-
var cssLines = cssSource.split('\n');
|
|
102
|
-
|
|
103
|
-
var errors = [];
|
|
104
|
-
var validatedMappings = 0;
|
|
105
|
-
|
|
106
|
-
// Validate mappings for each line in the CSS
|
|
107
|
-
for (var cssLine = 1; cssLine <= cssLines.length; cssLine++) {
|
|
108
|
-
var cssLineContent = cssLines[cssLine - 1];
|
|
109
|
-
// Skip empty lines
|
|
110
|
-
if (!cssLineContent.trim()) {
|
|
111
|
-
continue;
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
// Check mapping for the start of this CSS line
|
|
115
|
-
var mapping = consumer.originalPositionFor({
|
|
116
|
-
line: cssLine,
|
|
117
|
-
column: 0
|
|
118
|
-
});
|
|
119
|
-
|
|
120
|
-
if (mapping.source) {
|
|
121
|
-
validatedMappings++;
|
|
122
|
-
|
|
123
|
-
// Verify the source file exists in the sourcemap
|
|
124
|
-
if (!sourceMapObj.sources || sourceMapObj.sources.indexOf(mapping.source) === -1) {
|
|
125
|
-
errors.push('Line ' + cssLine + ': mapped to source "' + mapping.source + '" which is not in sources array');
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
// Verify the line number is valid
|
|
129
|
-
if (mapping.line && mapping.line > 0) {
|
|
130
|
-
// If we can find the source file, validate the line exists
|
|
131
|
-
var sourceIndex = sourceMapObj.sources.indexOf(mapping.source);
|
|
132
|
-
if (sourceIndex >= 0 && sourceMapObj.sourcesContent && sourceMapObj.sourcesContent[sourceIndex] !== undefined && sourceMapObj.sourcesContent[sourceIndex] !== null) {
|
|
133
|
-
var sourceContent = sourceMapObj.sourcesContent[sourceIndex];
|
|
134
|
-
// Ensure sourceContent is a string (it should be, but be defensive)
|
|
135
|
-
if (typeof sourceContent !== 'string') {
|
|
136
|
-
sourceContent = String(sourceContent);
|
|
137
|
-
}
|
|
138
|
-
// Split by newline - handle both \n and \r\n
|
|
139
|
-
var sourceLines = sourceContent.split(/\r?\n/);
|
|
140
|
-
if (mapping.line > sourceLines.length) {
|
|
141
|
-
errors.push('Line ' + cssLine + ': mapped to line ' + mapping.line + ' in "' + mapping.source + '" but source only has ' + sourceLines.length + ' lines');
|
|
142
|
-
}
|
|
143
|
-
} else if (sourceIndex >= 0) {
|
|
144
|
-
// Source content not embedded, try to validate against the actual file if it matches
|
|
145
|
-
// This is a best-effort validation
|
|
146
|
-
}
|
|
147
|
-
}
|
|
148
|
-
}
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
// Validate that all sources in the sourcemap are valid
|
|
152
|
-
if (sourceMapObj.sources) {
|
|
153
|
-
sourceMapObj.sources.forEach(function(source, index) {
|
|
154
|
-
if (sourceMapObj.sourcesContent && sourceMapObj.sourcesContent[index]) {
|
|
155
|
-
// Source content is embedded, validate it's not empty
|
|
156
|
-
if (!sourceMapObj.sourcesContent[index].trim()) {
|
|
157
|
-
errors.push('Source "' + source + '" has empty content');
|
|
158
|
-
}
|
|
159
|
-
}
|
|
160
|
-
});
|
|
161
|
-
}
|
|
162
|
-
|
|
163
|
-
if (consumer.destroy && typeof consumer.destroy === 'function') {
|
|
164
|
-
consumer.destroy();
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
return {
|
|
168
|
-
valid: errors.length === 0,
|
|
169
|
-
errors: errors,
|
|
170
|
-
mappingsValidated: validatedMappings
|
|
171
|
-
};
|
|
172
|
-
}
|
|
173
|
-
|
|
174
|
-
function testSourcemap(name, err, compiledLess, doReplacements, sourcemap, baseFolder, getFilename) {
|
|
175
|
-
if (err) {
|
|
176
|
-
fail('ERROR: ' + (err && err.message));
|
|
177
|
-
return;
|
|
178
|
-
}
|
|
179
|
-
// Check the sourceMappingURL at the bottom of the file
|
|
180
|
-
// Default expected URL is name + '.css.map', but can be overridden by sourceMapURL option
|
|
181
|
-
var sourceMappingPrefix = '/*# sourceMappingURL=',
|
|
182
|
-
sourceMappingSuffix = ' */';
|
|
183
|
-
var indexOfSourceMappingPrefix = compiledLess.indexOf(sourceMappingPrefix);
|
|
184
|
-
if (indexOfSourceMappingPrefix === -1) {
|
|
185
|
-
fail('ERROR: sourceMappingURL was not found in ' + baseFolder + '/' + name + '.css.');
|
|
186
|
-
return;
|
|
187
|
-
}
|
|
188
|
-
|
|
189
|
-
var startOfSourceMappingValue = indexOfSourceMappingPrefix + sourceMappingPrefix.length,
|
|
190
|
-
indexOfSuffix = compiledLess.indexOf(sourceMappingSuffix, startOfSourceMappingValue),
|
|
191
|
-
actualSourceMapURL = compiledLess.substring(startOfSourceMappingValue, indexOfSuffix === -1 ? compiledLess.length : indexOfSuffix).trim();
|
|
192
|
-
|
|
193
|
-
// For tests with custom sourceMapURL, we just verify it exists and is non-empty
|
|
194
|
-
// The actual value will be validated by comparing the sourcemap JSON
|
|
195
|
-
if (!actualSourceMapURL) {
|
|
196
|
-
fail('ERROR: sourceMappingURL is empty in ' + baseFolder + '/' + name + '.css.');
|
|
197
|
-
return;
|
|
198
|
-
}
|
|
199
|
-
|
|
200
|
-
// Use getFilename if available (for sourcemap tests with subdirectories)
|
|
201
|
-
var jsonPath;
|
|
202
|
-
if (getFilename && typeof getFilename === 'function') {
|
|
203
|
-
jsonPath = getFilename(name, 'sourcemap', baseFolder);
|
|
204
|
-
} else {
|
|
205
|
-
// Fallback: extract just the filename for sourcemap JSON files
|
|
206
|
-
var jsonFilename = path.basename(name);
|
|
207
|
-
jsonPath = path.join('test/sourcemaps', jsonFilename) + '.json';
|
|
208
|
-
}
|
|
209
|
-
fs.readFile(jsonPath, 'utf8', function (e, expectedSourcemap) {
|
|
210
|
-
process.stdout.write('- ' + path.join(baseFolder, name) + ': ');
|
|
211
|
-
if (e) {
|
|
212
|
-
fail('ERROR: Could not read expected sourcemap file: ' + jsonPath + ' - ' + e.message);
|
|
213
|
-
return;
|
|
214
|
-
}
|
|
215
|
-
|
|
216
|
-
// Apply doReplacements to the expected sourcemap to handle {path} placeholders
|
|
217
|
-
// This normalizes absolute paths that differ between environments
|
|
218
|
-
// For sourcemaps, we need to ensure {path} uses forward slashes to avoid breaking JSON
|
|
219
|
-
// (backslashes in JSON strings need escaping, and sourcemaps should use forward slashes anyway)
|
|
220
|
-
var replacementPath = path.join(path.dirname(path.join(baseFolder, name) + '.less'), '/');
|
|
221
|
-
// Normalize to forward slashes for sourcemap JSON (web-compatible)
|
|
222
|
-
replacementPath = replacementPath.replace(/\\/g, '/');
|
|
223
|
-
// Replace {path} with normalized forward-slash path BEFORE calling doReplacements
|
|
224
|
-
// This ensures the JSON is always valid and uses web-compatible paths
|
|
225
|
-
expectedSourcemap = expectedSourcemap.replace(/\{path\}/g, replacementPath);
|
|
226
|
-
// Also handle other placeholders that might be in the sourcemap (but {path} is already done)
|
|
227
|
-
expectedSourcemap = doReplacements(expectedSourcemap, baseFolder, path.join(baseFolder, name) + '.less');
|
|
228
|
-
|
|
229
|
-
// Normalize paths in sourcemap JSON to use forward slashes (web-compatible)
|
|
230
|
-
// We need to parse the JSON, normalize the file property, then stringify for comparison
|
|
231
|
-
// This avoids breaking escape sequences like \n in the JSON string
|
|
232
|
-
function normalizeSourcemapPaths(sm) {
|
|
233
|
-
try {
|
|
234
|
-
var parsed = typeof sm === 'string' ? JSON.parse(sm) : sm;
|
|
235
|
-
if (parsed.file) {
|
|
236
|
-
parsed.file = parsed.file.replace(/\\/g, '/');
|
|
237
|
-
}
|
|
238
|
-
// Also normalize paths in sources array
|
|
239
|
-
if (parsed.sources && Array.isArray(parsed.sources)) {
|
|
240
|
-
parsed.sources = parsed.sources.map(function(src) {
|
|
241
|
-
return src.replace(/\\/g, '/');
|
|
242
|
-
});
|
|
243
|
-
}
|
|
244
|
-
return JSON.stringify(parsed, null, 0);
|
|
245
|
-
} catch (parseErr) {
|
|
246
|
-
// If parsing fails, return original (shouldn't happen)
|
|
247
|
-
return sm;
|
|
248
|
-
}
|
|
249
|
-
}
|
|
250
|
-
|
|
251
|
-
var normalizedSourcemap = normalizeSourcemapPaths(sourcemap);
|
|
252
|
-
var normalizedExpected = normalizeSourcemapPaths(expectedSourcemap);
|
|
253
|
-
|
|
254
|
-
if (normalizedSourcemap === normalizedExpected) {
|
|
255
|
-
// Validate the sourcemap mappings are correct
|
|
256
|
-
// Find the actual LESS file - it might be in a subdirectory
|
|
257
|
-
var nameParts = name.split('/');
|
|
258
|
-
var lessFileName = nameParts[nameParts.length - 1];
|
|
259
|
-
var lessFileDir = nameParts.length > 1 ? nameParts.slice(0, -1).join('/') : '';
|
|
260
|
-
var lessFile = path.join(lessFolder, lessFileDir, lessFileName) + '.less';
|
|
261
|
-
|
|
262
|
-
// Only validate if the LESS file exists
|
|
263
|
-
if (fs.existsSync(lessFile)) {
|
|
264
|
-
try {
|
|
265
|
-
// Parse the sourcemap once for validation (avoid re-parsing)
|
|
266
|
-
// Use the original sourcemap string, not the normalized one
|
|
267
|
-
var sourceMapObjForValidation = typeof sourcemap === 'string' ? JSON.parse(sourcemap) : sourcemap;
|
|
268
|
-
var validation = validateSourcemapMappings(sourceMapObjForValidation, lessFile, compiledLess);
|
|
269
|
-
if (!validation.valid) {
|
|
270
|
-
fail('ERROR: Sourcemap validation failed:\n' + validation.errors.join('\n'));
|
|
271
|
-
return;
|
|
272
|
-
}
|
|
273
|
-
if (isVerbose && validation.mappingsValidated > 0) {
|
|
274
|
-
process.stdout.write(' (validated ' + validation.mappingsValidated + ' mappings)');
|
|
275
|
-
}
|
|
276
|
-
} catch (validationErr) {
|
|
277
|
-
if (isVerbose) {
|
|
278
|
-
process.stdout.write(' (validation error: ' + validationErr.message + ')');
|
|
279
|
-
}
|
|
280
|
-
// Don't fail the test if validation has an error, just log it
|
|
281
|
-
}
|
|
282
|
-
}
|
|
283
|
-
|
|
284
|
-
ok('OK');
|
|
285
|
-
} else if (err) {
|
|
286
|
-
fail('ERROR: ' + (err && err.message));
|
|
287
|
-
if (isVerbose) {
|
|
288
|
-
process.stdout.write('\n');
|
|
289
|
-
process.stdout.write(err.stack + '\n');
|
|
290
|
-
}
|
|
291
|
-
} else {
|
|
292
|
-
difference('FAIL', normalizedExpected, normalizedSourcemap);
|
|
293
|
-
}
|
|
294
|
-
});
|
|
295
|
-
}
|
|
296
|
-
|
|
297
|
-
function testSourcemapWithoutUrlAnnotation(name, err, compiledLess, doReplacements, sourcemap, baseFolder) {
|
|
298
|
-
if (err) {
|
|
299
|
-
fail('ERROR: ' + (err && err.message));
|
|
300
|
-
return;
|
|
301
|
-
}
|
|
302
|
-
// This matches with strings that end($) with source mapping url annotation.
|
|
303
|
-
var sourceMapRegExp = /\/\*# sourceMappingURL=.+\.css\.map \*\/$/;
|
|
304
|
-
if (sourceMapRegExp.test(compiledLess)) {
|
|
305
|
-
fail('ERROR: sourceMappingURL found in ' + baseFolder + '/' + name + '.css.');
|
|
306
|
-
return;
|
|
307
|
-
}
|
|
308
|
-
|
|
309
|
-
// Even if annotation is not necessary, the map file should be there.
|
|
310
|
-
fs.readFile(path.join('test/', name) + '.json', 'utf8', function (e, expectedSourcemap) {
|
|
311
|
-
process.stdout.write('- ' + path.join(baseFolder, name) + ': ');
|
|
312
|
-
if (sourcemap === expectedSourcemap) {
|
|
313
|
-
ok('OK');
|
|
314
|
-
} else if (err) {
|
|
315
|
-
fail('ERROR: ' + (err && err.message));
|
|
316
|
-
if (isVerbose) {
|
|
317
|
-
process.stdout.write('\n');
|
|
318
|
-
process.stdout.write(err.stack + '\n');
|
|
319
|
-
}
|
|
320
|
-
} else {
|
|
321
|
-
difference('FAIL', expectedSourcemap, sourcemap);
|
|
322
|
-
}
|
|
323
|
-
});
|
|
324
|
-
}
|
|
325
|
-
|
|
326
|
-
function testEmptySourcemap(name, err, compiledLess, doReplacements, sourcemap, baseFolder) {
|
|
327
|
-
process.stdout.write('- ' + path.join(baseFolder, name) + ': ');
|
|
328
|
-
if (err) {
|
|
329
|
-
fail('ERROR: ' + (err && err.message));
|
|
330
|
-
} else {
|
|
331
|
-
var expectedSourcemap = undefined;
|
|
332
|
-
if ( compiledLess !== '' ) {
|
|
333
|
-
difference('\nCompiledLess must be empty', '', compiledLess);
|
|
334
|
-
|
|
335
|
-
} else if (sourcemap !== expectedSourcemap) {
|
|
336
|
-
fail('Sourcemap must be undefined');
|
|
337
|
-
} else {
|
|
338
|
-
ok('OK');
|
|
339
|
-
}
|
|
340
|
-
}
|
|
341
|
-
}
|
|
342
|
-
|
|
343
|
-
function testSourcemapWithVariableInSelector(name, err, compiledLess, doReplacements, sourcemap, baseFolder) {
|
|
344
|
-
if (err) {
|
|
345
|
-
fail('ERROR: ' + (err && err.message));
|
|
346
|
-
return;
|
|
347
|
-
}
|
|
348
|
-
|
|
349
|
-
// Even if annotation is not necessary, the map file should be there.
|
|
350
|
-
fs.readFile(path.join('test/', name) + '.json', 'utf8', function (e, expectedSourcemap) {
|
|
351
|
-
process.stdout.write('- ' + path.join(baseFolder, name) + ': ');
|
|
352
|
-
if (sourcemap === expectedSourcemap) {
|
|
353
|
-
ok('OK');
|
|
354
|
-
} else if (err) {
|
|
355
|
-
fail('ERROR: ' + (err && err.message));
|
|
356
|
-
if (isVerbose) {
|
|
357
|
-
process.stdout.write('\n');
|
|
358
|
-
process.stdout.write(err.stack + '\n');
|
|
359
|
-
}
|
|
360
|
-
} else {
|
|
361
|
-
difference('FAIL', expectedSourcemap, sourcemap);
|
|
362
|
-
}
|
|
363
|
-
});
|
|
364
|
-
}
|
|
365
|
-
|
|
366
|
-
function testImports(name, err, compiledLess, doReplacements, sourcemap, baseFolder, imports) {
|
|
367
|
-
if (err) {
|
|
368
|
-
fail('ERROR: ' + (err && err.message));
|
|
369
|
-
return;
|
|
370
|
-
}
|
|
371
|
-
|
|
372
|
-
function stringify(str) {
|
|
373
|
-
return JSON.stringify(imports, null, ' ')
|
|
374
|
-
}
|
|
375
|
-
|
|
376
|
-
/** Imports are not sorted */
|
|
377
|
-
const importsString = stringify(imports.sort())
|
|
378
|
-
|
|
379
|
-
fs.readFile(path.join(lessFolder, name) + '.json', 'utf8', function (e, expectedImports) {
|
|
380
|
-
if (e) {
|
|
381
|
-
fail('ERROR: ' + (e && e.message));
|
|
382
|
-
return;
|
|
383
|
-
}
|
|
384
|
-
process.stdout.write('- ' + path.join(baseFolder, name) + ': ');
|
|
385
|
-
expectedImports = stringify(JSON.parse(expectedImports).sort());
|
|
386
|
-
expectedImports = globalReplacements(expectedImports, baseFolder);
|
|
387
|
-
|
|
388
|
-
if (expectedImports === importsString) {
|
|
389
|
-
ok('OK');
|
|
390
|
-
} else if (err) {
|
|
391
|
-
fail('ERROR: ' + (err && err.message));
|
|
392
|
-
if (isVerbose) {
|
|
393
|
-
process.stdout.write('\n');
|
|
394
|
-
process.stdout.write(err.stack + '\n');
|
|
395
|
-
}
|
|
396
|
-
} else {
|
|
397
|
-
difference('FAIL', expectedImports, importsString);
|
|
398
|
-
}
|
|
399
|
-
});
|
|
400
|
-
}
|
|
401
|
-
|
|
402
|
-
function testErrors(name, err, compiledLess, doReplacements, sourcemap, baseFolder) {
|
|
403
|
-
fs.readFile(path.join(baseFolder, name) + '.txt', 'utf8', function (e, expectedErr) {
|
|
404
|
-
process.stdout.write('- ' + path.join(baseFolder, name) + ': ');
|
|
405
|
-
expectedErr = doReplacements(expectedErr, baseFolder, err && err.filename);
|
|
406
|
-
if (!err) {
|
|
407
|
-
if (compiledLess) {
|
|
408
|
-
fail('No Error', 'red');
|
|
409
|
-
} else {
|
|
410
|
-
fail('No Error, No Output');
|
|
411
|
-
}
|
|
412
|
-
} else {
|
|
413
|
-
var errMessage = err.toString();
|
|
414
|
-
if (errMessage === expectedErr) {
|
|
415
|
-
ok('OK');
|
|
416
|
-
} else {
|
|
417
|
-
difference('FAIL', expectedErr, errMessage);
|
|
418
|
-
}
|
|
419
|
-
}
|
|
420
|
-
});
|
|
421
|
-
}
|
|
422
|
-
|
|
423
|
-
// To fix ci fail about error format change in upstream v8 project
|
|
424
|
-
// https://github.com/v8/v8/commit/c0fd89c3c089e888c4f4e8582e56db7066fa779b
|
|
425
|
-
// Node 16.9.0+ include this change via https://github.com/nodejs/node/pull/39947
|
|
426
|
-
function testTypeErrors(name, err, compiledLess, doReplacements, sourcemap, baseFolder) {
|
|
427
|
-
const fileSuffix = semver.gte(process.version, 'v16.9.0') ? '-2.txt' : '.txt';
|
|
428
|
-
fs.readFile(path.join(baseFolder, name) + fileSuffix, 'utf8', function (e, expectedErr) {
|
|
429
|
-
process.stdout.write('- ' + path.join(baseFolder, name) + ': ');
|
|
430
|
-
expectedErr = doReplacements(expectedErr, baseFolder, err && err.filename);
|
|
431
|
-
if (!err) {
|
|
432
|
-
if (compiledLess) {
|
|
433
|
-
fail('No Error', 'red');
|
|
434
|
-
} else {
|
|
435
|
-
fail('No Error, No Output');
|
|
436
|
-
}
|
|
437
|
-
} else {
|
|
438
|
-
var errMessage = err.toString();
|
|
439
|
-
if (errMessage === expectedErr) {
|
|
440
|
-
ok('OK');
|
|
441
|
-
} else {
|
|
442
|
-
difference('FAIL', expectedErr, errMessage);
|
|
443
|
-
}
|
|
444
|
-
}
|
|
445
|
-
});
|
|
446
|
-
}
|
|
447
|
-
|
|
448
|
-
// https://github.com/less/less.js/issues/3112
|
|
449
|
-
function testJSImport() {
|
|
450
|
-
process.stdout.write('- Testing root function registry');
|
|
451
|
-
less.functions.functionRegistry.add('ext', function() {
|
|
452
|
-
return new less.tree.Anonymous('file');
|
|
453
|
-
});
|
|
454
|
-
var expected = '@charset "utf-8";\n';
|
|
455
|
-
toCSS({}, path.join(lessFolder, 'tests-config', 'root-registry', 'root.less'), function(error, output) {
|
|
456
|
-
if (error) {
|
|
457
|
-
return fail('ERROR: ' + error);
|
|
458
|
-
}
|
|
459
|
-
if (output.css === expected) {
|
|
460
|
-
return ok('OK');
|
|
461
|
-
}
|
|
462
|
-
difference('FAIL', expected, output.css);
|
|
463
|
-
});
|
|
464
|
-
}
|
|
465
|
-
|
|
466
|
-
function globalReplacements(input, directory, filename) {
|
|
467
|
-
var path = require('path');
|
|
468
|
-
var p = filename ? path.join(path.dirname(filename), '/') : directory;
|
|
469
|
-
|
|
470
|
-
// For debug tests in subdirectories (comments/, mediaquery/, all/),
|
|
471
|
-
// the import/ directory and main linenumbers.less file are at the parent debug/ level, not in the subdirectory
|
|
472
|
-
var isDebugSubdirectory = false;
|
|
473
|
-
var debugParentPath = null;
|
|
474
|
-
|
|
475
|
-
if (directory) {
|
|
476
|
-
// Normalize directory path separators for matching
|
|
477
|
-
var normalizedDir = directory.replace(/\\/g, '/');
|
|
478
|
-
// Check if we're in a debug subdirectory
|
|
479
|
-
if (normalizedDir.includes('/debug/') && (normalizedDir.includes('/comments/') || normalizedDir.includes('/mediaquery/') || normalizedDir.includes('/all/'))) {
|
|
480
|
-
isDebugSubdirectory = true;
|
|
481
|
-
// Extract the debug/ directory path (parent of the subdirectory)
|
|
482
|
-
// Match everything up to and including /debug/ (works with both absolute and relative paths)
|
|
483
|
-
var debugMatch = normalizedDir.match(/(.+\/debug)\//);
|
|
484
|
-
if (debugMatch) {
|
|
485
|
-
debugParentPath = debugMatch[1];
|
|
486
|
-
}
|
|
487
|
-
}
|
|
488
|
-
}
|
|
489
|
-
|
|
490
|
-
if (isDebugSubdirectory && debugParentPath) {
|
|
491
|
-
// For {path} placeholder, use the parent debug/ directory
|
|
492
|
-
// Convert back to native path format
|
|
493
|
-
p = debugParentPath.replace(/\//g, path.sep) + path.sep;
|
|
494
|
-
}
|
|
495
|
-
|
|
496
|
-
var pathimport;
|
|
497
|
-
if (isDebugSubdirectory && debugParentPath) {
|
|
498
|
-
pathimport = path.join(debugParentPath.replace(/\//g, path.sep), 'import') + path.sep;
|
|
499
|
-
} else {
|
|
500
|
-
pathimport = path.join(directory + 'import/');
|
|
501
|
-
}
|
|
502
|
-
|
|
503
|
-
var pathesc = p.replace(/[.:/\\]/g, function(a) { return '\\' + (a == '\\' ? '\/' : a); }),
|
|
504
|
-
pathimportesc = pathimport.replace(/[.:/\\]/g, function(a) { return '\\' + (a == '\\' ? '\/' : a); });
|
|
505
|
-
|
|
506
|
-
return input.replace(/\{path\}/g, p)
|
|
507
|
-
.replace(/\{node\}/g, '')
|
|
508
|
-
.replace(/\{\/node\}/g, '')
|
|
509
|
-
.replace(/\{pathhref\}/g, '')
|
|
510
|
-
.replace(/\{404status\}/g, '')
|
|
511
|
-
.replace(/\{nodepath\}/g, path.join(process.cwd(), 'node_modules', '/'))
|
|
512
|
-
.replace(/\{pathrel\}/g, path.join(path.relative(lessFolder, p), '/'))
|
|
513
|
-
.replace(/\{pathesc\}/g, pathesc)
|
|
514
|
-
.replace(/\{pathimport\}/g, pathimport)
|
|
515
|
-
.replace(/\{pathimportesc\}/g, pathimportesc)
|
|
516
|
-
.replace(/\r\n/g, '\n');
|
|
517
|
-
}
|
|
518
|
-
|
|
519
|
-
function checkGlobalLeaks() {
|
|
520
|
-
return Object.keys(global).filter(function(v) {
|
|
521
|
-
return globals.indexOf(v) < 0;
|
|
522
|
-
});
|
|
523
|
-
}
|
|
524
|
-
|
|
525
|
-
function testSyncronous(options, filenameNoExtension) {
|
|
526
|
-
if (oneTestOnly && ('Test Sync ' + filenameNoExtension) !== oneTestOnly) {
|
|
527
|
-
return;
|
|
528
|
-
}
|
|
529
|
-
totalTests++;
|
|
530
|
-
queue(function() {
|
|
531
|
-
var isSync = true;
|
|
532
|
-
toCSS(options, path.join(lessFolder, filenameNoExtension + '.less'), function (err, result) {
|
|
533
|
-
process.stdout.write('- Test Sync ' + filenameNoExtension + ': ');
|
|
534
|
-
|
|
535
|
-
if (isSync) {
|
|
536
|
-
ok('OK');
|
|
537
|
-
} else {
|
|
538
|
-
fail('Not Sync');
|
|
539
|
-
}
|
|
540
|
-
release();
|
|
541
|
-
});
|
|
542
|
-
isSync = false;
|
|
543
|
-
});
|
|
544
|
-
}
|
|
545
|
-
|
|
546
|
-
function runTestSet(options, foldername, verifyFunction, nameModifier, doReplacements, getFilename) {
|
|
547
|
-
// Handle case where first parameter is glob patterns (no options object)
|
|
548
|
-
if (Array.isArray(options)) {
|
|
549
|
-
// First parameter is glob patterns, no options object
|
|
550
|
-
foldername = options;
|
|
551
|
-
options = {};
|
|
552
|
-
} else if (typeof options === 'string') {
|
|
553
|
-
// First parameter is foldername (no options object)
|
|
554
|
-
foldername = options;
|
|
555
|
-
options = {};
|
|
556
|
-
} else {
|
|
557
|
-
options = options ? clone(options) : {};
|
|
558
|
-
}
|
|
559
|
-
runTestSetInternal(lessFolder, options, foldername, verifyFunction, nameModifier, doReplacements, getFilename);
|
|
560
|
-
}
|
|
561
|
-
|
|
562
|
-
function runTestSetNormalOnly(options, foldername, verifyFunction, nameModifier, doReplacements, getFilename) {
|
|
563
|
-
runTestSetInternal(lessFolder, options, foldername, verifyFunction, nameModifier, doReplacements, getFilename);
|
|
564
|
-
}
|
|
565
|
-
|
|
566
|
-
function runTestSetInternal(baseFolder, opts, foldername, verifyFunction, nameModifier, doReplacements, getFilename) {
|
|
567
|
-
foldername = foldername || '';
|
|
568
|
-
|
|
569
|
-
var originalOptions = opts || {};
|
|
570
|
-
|
|
571
|
-
if (!doReplacements) {
|
|
572
|
-
doReplacements = globalReplacements;
|
|
573
|
-
}
|
|
574
|
-
|
|
575
|
-
// Handle glob patterns with exclusions
|
|
576
|
-
if (Array.isArray(foldername)) {
|
|
577
|
-
var patterns = foldername;
|
|
578
|
-
var includePatterns = [];
|
|
579
|
-
var excludePatterns = [];
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
patterns.forEach(function(pattern) {
|
|
583
|
-
if (pattern.startsWith('!')) {
|
|
584
|
-
excludePatterns.push(pattern.substring(1));
|
|
585
|
-
} else {
|
|
586
|
-
includePatterns.push(pattern);
|
|
587
|
-
}
|
|
588
|
-
});
|
|
589
|
-
|
|
590
|
-
// Use glob to find all matching files, excluding the excluded patterns
|
|
591
|
-
var allFiles = [];
|
|
592
|
-
includePatterns.forEach(function(pattern) {
|
|
593
|
-
var files = glob.sync(pattern, {
|
|
594
|
-
cwd: baseFolder,
|
|
595
|
-
absolute: true,
|
|
596
|
-
ignore: excludePatterns
|
|
597
|
-
});
|
|
598
|
-
|
|
599
|
-
allFiles = allFiles.concat(files);
|
|
600
|
-
});
|
|
601
|
-
|
|
602
|
-
// Note: needle mocking is set up globally in index.js
|
|
603
|
-
|
|
604
|
-
// Process each .less file found
|
|
605
|
-
allFiles.forEach(function(filePath) {
|
|
606
|
-
if (/\.less$/.test(filePath)) {
|
|
607
|
-
var file = path.basename(filePath);
|
|
608
|
-
// For glob patterns, we need to construct the relative path differently
|
|
609
|
-
// The filePath is absolute, so we need to get the path relative to the test-data directory
|
|
610
|
-
var relativePath = path.relative(baseFolder, path.dirname(filePath)) + '/';
|
|
611
|
-
|
|
612
|
-
// Only process files that have corresponding .css files (these are the actual tests)
|
|
613
|
-
var cssPath = path.join(path.dirname(filePath), path.basename(file, '.less') + '.css');
|
|
614
|
-
if (fs.existsSync(cssPath)) {
|
|
615
|
-
// Process this file using the existing logic
|
|
616
|
-
processFileWithInfo({
|
|
617
|
-
file: file,
|
|
618
|
-
fullPath: filePath,
|
|
619
|
-
relativePath: relativePath
|
|
620
|
-
});
|
|
621
|
-
}
|
|
622
|
-
}
|
|
623
|
-
});
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
return;
|
|
627
|
-
}
|
|
628
|
-
|
|
629
|
-
function processFileWithInfo(fileInfo) {
|
|
630
|
-
var file = fileInfo.file;
|
|
631
|
-
var fullPath = fileInfo.fullPath;
|
|
632
|
-
var relativePath = fileInfo.relativePath;
|
|
633
|
-
|
|
634
|
-
// Load config for this specific file using cosmiconfig
|
|
635
|
-
var configResult = cosmiconfigSync('styles').search(path.dirname(fullPath));
|
|
636
|
-
|
|
637
|
-
// Deep clone the original options to prevent Less from modifying shared objects
|
|
638
|
-
var options = JSON.parse(JSON.stringify(originalOptions || {}));
|
|
639
|
-
|
|
640
|
-
if (configResult && configResult.config && configResult.config.language && configResult.config.language.less) {
|
|
641
|
-
// Deep clone and merge the language.less settings with the original options
|
|
642
|
-
var lessConfig = JSON.parse(JSON.stringify(configResult.config.language.less));
|
|
643
|
-
Object.keys(lessConfig).forEach(function(key) {
|
|
644
|
-
options[key] = lessConfig[key];
|
|
645
|
-
});
|
|
646
|
-
}
|
|
647
|
-
|
|
648
|
-
// Merge any lessOptions from the testMap (for dynamic options like getVars functions)
|
|
649
|
-
if (originalOptions && originalOptions.lessOptions) {
|
|
650
|
-
Object.keys(originalOptions.lessOptions).forEach(function(key) {
|
|
651
|
-
var value = originalOptions.lessOptions[key];
|
|
652
|
-
if (typeof value === 'function') {
|
|
653
|
-
// For functions, call them with the file path
|
|
654
|
-
var result = value(fullPath);
|
|
655
|
-
options[key] = result;
|
|
656
|
-
} else {
|
|
657
|
-
// For static values, use them directly
|
|
658
|
-
options[key] = value;
|
|
659
|
-
}
|
|
660
|
-
});
|
|
661
|
-
}
|
|
662
|
-
|
|
663
|
-
// Don't pass stylize to less.render as it's not a valid option
|
|
664
|
-
|
|
665
|
-
var name = getBasename(file, relativePath);
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
if (oneTestOnly && typeof oneTestOnly === 'string' && !name.includes(oneTestOnly)) {
|
|
669
|
-
return;
|
|
670
|
-
}
|
|
671
|
-
|
|
672
|
-
totalTests++;
|
|
673
|
-
|
|
674
|
-
if (options.sourceMap && !options.sourceMap.sourceMapFileInline) {
|
|
675
|
-
// Set test infrastructure defaults only if not already set by styles.config.cjs
|
|
676
|
-
// Less.js core (parse-tree.js) will handle normalization of:
|
|
677
|
-
// - sourceMapBasepath (defaults to input file's directory)
|
|
678
|
-
// - sourceMapInputFilename (defaults to options.filename)
|
|
679
|
-
// - sourceMapFilename (derived from sourceMapOutputFilename or input filename)
|
|
680
|
-
// - sourceMapOutputFilename (derived from input filename if not set)
|
|
681
|
-
if (!options.sourceMap.sourceMapOutputFilename) {
|
|
682
|
-
// Needed for sourcemap file name in JSON output
|
|
683
|
-
options.sourceMap.sourceMapOutputFilename = name + '.css';
|
|
684
|
-
}
|
|
685
|
-
if (!options.sourceMap.sourceMapRootpath) {
|
|
686
|
-
// Test-specific default for consistent test output paths
|
|
687
|
-
options.sourceMap.sourceMapRootpath = 'testweb/';
|
|
688
|
-
}
|
|
689
|
-
}
|
|
690
|
-
|
|
691
|
-
options.getVars = function(file) {
|
|
692
|
-
try {
|
|
693
|
-
return JSON.parse(fs.readFileSync(getFilename(getBasename(file, relativePath), 'vars', baseFolder), 'utf8'));
|
|
694
|
-
}
|
|
695
|
-
catch (e) {
|
|
696
|
-
return {};
|
|
697
|
-
}
|
|
698
|
-
};
|
|
699
|
-
|
|
700
|
-
var doubleCallCheck = false;
|
|
701
|
-
queue(function() {
|
|
702
|
-
toCSS(options, fullPath, function (err, result) {
|
|
703
|
-
|
|
704
|
-
if (doubleCallCheck) {
|
|
705
|
-
totalTests++;
|
|
706
|
-
fail('less is calling back twice');
|
|
707
|
-
process.stdout.write(doubleCallCheck + '\n');
|
|
708
|
-
process.stdout.write((new Error()).stack + '\n');
|
|
709
|
-
return;
|
|
710
|
-
}
|
|
711
|
-
doubleCallCheck = (new Error()).stack;
|
|
712
|
-
|
|
713
|
-
/**
|
|
714
|
-
* @todo - refactor so the result object is sent to the verify function
|
|
715
|
-
*/
|
|
716
|
-
if (verifyFunction) {
|
|
717
|
-
var verificationResult = verifyFunction(
|
|
718
|
-
name, err, result && result.css, doReplacements, result && result.map, baseFolder, result && result.imports, getFilename
|
|
719
|
-
);
|
|
720
|
-
release();
|
|
721
|
-
return verificationResult;
|
|
722
|
-
}
|
|
723
|
-
|
|
724
|
-
if (err) {
|
|
725
|
-
fail('ERROR: ' + (err && err.message));
|
|
726
|
-
if (isVerbose) {
|
|
727
|
-
process.stdout.write('\n');
|
|
728
|
-
if (err.stack) {
|
|
729
|
-
process.stdout.write(err.stack + '\n');
|
|
730
|
-
} else {
|
|
731
|
-
// this sometimes happen - show the whole error object
|
|
732
|
-
console.log(err);
|
|
733
|
-
}
|
|
734
|
-
}
|
|
735
|
-
release();
|
|
736
|
-
return;
|
|
737
|
-
}
|
|
738
|
-
var css_name = name;
|
|
739
|
-
if (nameModifier) { css_name = nameModifier(name); }
|
|
740
|
-
|
|
741
|
-
// Check if we're using the new co-located structure (tests-unit/ or tests-config/) or the old separated structure
|
|
742
|
-
var cssPath;
|
|
743
|
-
if (relativePath.startsWith('tests-unit/') || relativePath.startsWith('tests-config/')) {
|
|
744
|
-
// New co-located structure: CSS file is in the same directory as LESS file
|
|
745
|
-
cssPath = path.join(path.dirname(fullPath), path.basename(file, '.less') + '.css');
|
|
746
|
-
} else {
|
|
747
|
-
// Old separated structure: CSS file is in separate css/ folder
|
|
748
|
-
// Windows compatibility: css_name may already contain path separators
|
|
749
|
-
// Use path.join with empty string to let path.join handle normalization
|
|
750
|
-
cssPath = path.join(testFolder, css_name) + '.css';
|
|
751
|
-
}
|
|
752
|
-
|
|
753
|
-
// For the new structure, we need to handle replacements differently
|
|
754
|
-
var replacementPath;
|
|
755
|
-
if (relativePath.startsWith('tests-unit/') || relativePath.startsWith('tests-config/')) {
|
|
756
|
-
replacementPath = path.dirname(fullPath);
|
|
757
|
-
// Ensure replacementPath ends with a path separator for consistent matching
|
|
758
|
-
if (!replacementPath.endsWith(path.sep)) {
|
|
759
|
-
replacementPath += path.sep;
|
|
760
|
-
}
|
|
761
|
-
} else {
|
|
762
|
-
replacementPath = path.join(baseFolder, relativePath);
|
|
763
|
-
}
|
|
764
|
-
|
|
765
|
-
var testName = fullPath.replace(/\.less$/, '');
|
|
766
|
-
process.stdout.write('- ' + testName + ': ');
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
var css = fs.readFileSync(cssPath, 'utf8');
|
|
770
|
-
css = css && doReplacements(css, replacementPath);
|
|
771
|
-
if (result.css === css) { ok('OK'); }
|
|
772
|
-
else {
|
|
773
|
-
difference('FAIL', css, result.css);
|
|
774
|
-
}
|
|
775
|
-
|
|
776
|
-
release();
|
|
777
|
-
});
|
|
778
|
-
});
|
|
779
|
-
}
|
|
780
|
-
|
|
781
|
-
function getBasename(file, relativePath) {
|
|
782
|
-
var basePath = relativePath || foldername;
|
|
783
|
-
// Ensure basePath ends with a slash for proper path construction
|
|
784
|
-
if (basePath.charAt(basePath.length - 1) !== '/') {
|
|
785
|
-
basePath = basePath + '/';
|
|
786
|
-
}
|
|
787
|
-
return basePath + path.basename(file, '.less');
|
|
788
|
-
}
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
// This function is only called for non-glob patterns now
|
|
792
|
-
// For glob patterns, we use the glob library in the calling code
|
|
793
|
-
var dirPath = path.join(baseFolder, foldername);
|
|
794
|
-
var items = fs.readdirSync(dirPath);
|
|
795
|
-
|
|
796
|
-
items.forEach(function(item) {
|
|
797
|
-
if (/\.less$/.test(item)) {
|
|
798
|
-
processFileWithInfo({
|
|
799
|
-
file: item,
|
|
800
|
-
fullPath: path.join(dirPath, item),
|
|
801
|
-
relativePath: foldername
|
|
802
|
-
});
|
|
803
|
-
}
|
|
804
|
-
});
|
|
805
|
-
}
|
|
806
|
-
|
|
807
|
-
function diff(left, right) {
|
|
808
|
-
// Configure chalk to always show colors
|
|
809
|
-
var chalk = require('chalk');
|
|
810
|
-
chalk.level = 3; // Force colors on
|
|
811
|
-
|
|
812
|
-
// Use jest-diff for much clearer output like Vitest
|
|
813
|
-
var diffResult = require('jest-diff').diffStringsUnified(left || '', right || '', {
|
|
814
|
-
expand: false,
|
|
815
|
-
includeChangeCounts: true,
|
|
816
|
-
contextLines: 1,
|
|
817
|
-
aColor: chalk.red,
|
|
818
|
-
bColor: chalk.green,
|
|
819
|
-
changeColor: chalk.inverse,
|
|
820
|
-
commonColor: chalk.dim
|
|
821
|
-
});
|
|
822
|
-
|
|
823
|
-
// jest-diff returns a string with ANSI colors, so we can output it directly
|
|
824
|
-
process.stdout.write(diffResult + '\n');
|
|
825
|
-
}
|
|
826
|
-
|
|
827
|
-
function fail(msg) {
|
|
828
|
-
process.stdout.write(stylize(msg, 'red') + '\n');
|
|
829
|
-
failedTests++;
|
|
830
|
-
endTest();
|
|
831
|
-
}
|
|
832
|
-
|
|
833
|
-
function difference(msg, left, right) {
|
|
834
|
-
process.stdout.write(stylize(msg, 'yellow') + '\n');
|
|
835
|
-
failedTests++;
|
|
836
|
-
|
|
837
|
-
// Only show the diff, not the full text
|
|
838
|
-
process.stdout.write(stylize('Diff:', 'yellow') + '\n');
|
|
839
|
-
|
|
840
|
-
diff(left || '', right || '');
|
|
841
|
-
endTest();
|
|
842
|
-
}
|
|
843
|
-
|
|
844
|
-
function ok(msg) {
|
|
845
|
-
process.stdout.write(stylize(msg, 'green') + '\n');
|
|
846
|
-
passedTests++;
|
|
847
|
-
endTest();
|
|
848
|
-
}
|
|
849
|
-
|
|
850
|
-
function finished() {
|
|
851
|
-
isFinished = true;
|
|
852
|
-
endTest();
|
|
853
|
-
}
|
|
854
|
-
|
|
855
|
-
function endTest() {
|
|
856
|
-
if (isFinished && ((failedTests + passedTests) >= totalTests)) {
|
|
857
|
-
clearInterval(finishTimer);
|
|
858
|
-
var leaked = checkGlobalLeaks();
|
|
859
|
-
process.stdout.write('\n');
|
|
860
|
-
if (failedTests > 0) {
|
|
861
|
-
process.stdout.write(failedTests + stylize(' Failed', 'red') + ', ' + passedTests + ' passed\n');
|
|
862
|
-
} else {
|
|
863
|
-
process.stdout.write(stylize('All Passed ', 'green') + passedTests + ' run\n');
|
|
864
|
-
}
|
|
865
|
-
if (leaked.length > 0) {
|
|
866
|
-
process.stdout.write('\n');
|
|
867
|
-
process.stdout.write(stylize('Global leak detected: ', 'red') + leaked.join(', ') + '\n');
|
|
868
|
-
}
|
|
869
|
-
|
|
870
|
-
if (leaked.length || failedTests) {
|
|
871
|
-
process.on('exit', function() { process.reallyExit(1); });
|
|
872
|
-
}
|
|
873
|
-
}
|
|
874
|
-
}
|
|
875
|
-
|
|
876
|
-
function contains(fullArray, obj) {
|
|
877
|
-
for (var i = 0; i < fullArray.length; i++) {
|
|
878
|
-
if (fullArray[i] === obj) {
|
|
879
|
-
return true;
|
|
880
|
-
}
|
|
881
|
-
}
|
|
882
|
-
return false;
|
|
883
|
-
}
|
|
884
|
-
|
|
885
|
-
/**
|
|
886
|
-
*
|
|
887
|
-
* @param {Object} options
|
|
888
|
-
* @param {string} filePath
|
|
889
|
-
* @param {Function} callback
|
|
890
|
-
*/
|
|
891
|
-
function toCSS(options, filePath, callback) {
|
|
892
|
-
// Deep clone options to prevent modifying the original, but preserve functions
|
|
893
|
-
var originalOptions = options || {};
|
|
894
|
-
options = JSON.parse(JSON.stringify(originalOptions));
|
|
895
|
-
|
|
896
|
-
// Restore functions that were lost in JSON serialization
|
|
897
|
-
if (originalOptions.getVars) {
|
|
898
|
-
options.getVars = originalOptions.getVars;
|
|
899
|
-
}
|
|
900
|
-
var str = fs.readFileSync(filePath, 'utf8'), addPath = path.dirname(filePath);
|
|
901
|
-
|
|
902
|
-
// Initialize paths array if it doesn't exist
|
|
903
|
-
if (typeof options.paths !== 'string') {
|
|
904
|
-
options.paths = options.paths || [];
|
|
905
|
-
} else {
|
|
906
|
-
options.paths = [options.paths];
|
|
907
|
-
}
|
|
908
|
-
|
|
909
|
-
// Add the current directory to paths if not already present
|
|
910
|
-
if (!contains(options.paths, addPath)) {
|
|
911
|
-
options.paths.push(addPath);
|
|
912
|
-
}
|
|
913
|
-
|
|
914
|
-
// Resolve all paths relative to the test file's directory
|
|
915
|
-
options.paths = options.paths.map(searchPath => {
|
|
916
|
-
if (path.isAbsolute(searchPath)) {
|
|
917
|
-
return searchPath;
|
|
918
|
-
}
|
|
919
|
-
// Resolve relative to the test file's directory
|
|
920
|
-
return path.resolve(path.dirname(filePath), searchPath);
|
|
921
|
-
})
|
|
922
|
-
|
|
923
|
-
options.filename = path.resolve(process.cwd(), filePath);
|
|
924
|
-
options.optimization = options.optimization || 0;
|
|
925
|
-
|
|
926
|
-
// Note: globalVars and modifyVars are now handled via styles.config.cjs or lessOptions
|
|
927
|
-
if (options.plugin) {
|
|
928
|
-
var Plugin = require(path.resolve(process.cwd(), options.plugin));
|
|
929
|
-
options.plugins = [Plugin];
|
|
930
|
-
}
|
|
931
|
-
less.render(str, options, callback);
|
|
932
|
-
}
|
|
933
|
-
|
|
934
|
-
function testNoOptions() {
|
|
935
|
-
if (oneTestOnly && 'Integration' !== oneTestOnly) {
|
|
936
|
-
return;
|
|
937
|
-
}
|
|
938
|
-
totalTests++;
|
|
939
|
-
try {
|
|
940
|
-
process.stdout.write('- Integration - creating parser without options: ');
|
|
941
|
-
less.render('');
|
|
942
|
-
} catch (e) {
|
|
943
|
-
fail(stylize('FAIL\n', 'red'));
|
|
944
|
-
return;
|
|
945
|
-
}
|
|
946
|
-
ok(stylize('OK\n', 'green'));
|
|
947
|
-
}
|
|
948
|
-
|
|
949
|
-
// HTTP redirect testing is now handled directly in test/index.js
|
|
950
|
-
|
|
951
|
-
function testDisablePluginRule() {
|
|
952
|
-
less.render(
|
|
953
|
-
'@plugin "../../plugin/some_plugin";',
|
|
954
|
-
{disablePluginRule: true},
|
|
955
|
-
function(err) {
|
|
956
|
-
// TODO: Need a better way of identifing exactly which error is thrown. Checking
|
|
957
|
-
// text like this tends to be rather brittle.
|
|
958
|
-
const EXPECTED = '@plugin statements are not allowed when disablePluginRule is set to true';
|
|
959
|
-
if (!err || String(err).indexOf(EXPECTED) < 0) {
|
|
960
|
-
fail('ERROR: Expected "' + EXPECTED + '" error');
|
|
961
|
-
return;
|
|
962
|
-
}
|
|
963
|
-
ok(stylize('OK\n', 'green'));
|
|
964
|
-
}
|
|
965
|
-
);
|
|
966
|
-
}
|
|
967
|
-
|
|
968
|
-
return {
|
|
969
|
-
runTestSet: runTestSet,
|
|
970
|
-
runTestSetNormalOnly: runTestSetNormalOnly,
|
|
971
|
-
testSyncronous: testSyncronous,
|
|
972
|
-
testErrors: testErrors,
|
|
973
|
-
testTypeErrors: testTypeErrors,
|
|
974
|
-
testSourcemap: testSourcemap,
|
|
975
|
-
testSourcemapWithoutUrlAnnotation: testSourcemapWithoutUrlAnnotation,
|
|
976
|
-
testSourcemapWithVariableInSelector: testSourcemapWithVariableInSelector,
|
|
977
|
-
testImports: testImports,
|
|
978
|
-
testEmptySourcemap: testEmptySourcemap,
|
|
979
|
-
testNoOptions: testNoOptions,
|
|
980
|
-
testDisablePluginRule: testDisablePluginRule,
|
|
981
|
-
testJSImport: testJSImport,
|
|
982
|
-
finished: finished
|
|
983
|
-
};
|
|
984
|
-
};
|