From 716101256e3b646e1e977c424c4bbd10fe3be3c3 Mon Sep 17 00:00:00 2001 From: Oliver Becker Date: Sun, 27 Oct 2013 12:47:59 +0100 Subject: [PATCH] added gradle based test suite, reviewed rhino integration --- .gitignore | 2 + Makefile | 9 +- build.gradle | 152 ++++++++++ build/rhino-header.js | 8 +- build/rhino-path.js | 35 +++ lib/less/functions.js | 2 +- lib/less/rhino.js | 382 ++++++++++++++++++++++---- test/less/errors/javascript-error.txt | 2 +- test/rhino/test-header.js | 13 + 9 files changed, 548 insertions(+), 57 deletions(-) diff --git a/.gitignore b/.gitignore index 6b5c221c..222f979b 100644 --- a/.gitignore +++ b/.gitignore @@ -4,7 +4,9 @@ node_modules *~ .#* .idea +.gradle test/browser/less.js test/browser/test-runner-*.htm test/sourcemaps/*.map test/sourcemaps/*.css +out diff --git a/Makefile b/Makefile index ed898a9e..979bc15c 100644 --- a/Makefile +++ b/Makefile @@ -74,17 +74,18 @@ rhino: @@touch ${RHINO} @@cat build/require-rhino.js\ build/rhino-header.js\ + build/rhino-path.js\ ${SRC}/parser.js\ + ${SRC}/functions.js\ + ${SRC}/colors.js\ + ${SRC}/tree.js\ + ${SRC}/tree/*.js\ ${SRC}/env.js\ ${SRC}/visitor.js\ ${SRC}/import-visitor.js\ ${SRC}/join-selector-visitor.js\ ${SRC}/to-css-visitor.js\ ${SRC}/extend-visitor.js\ - ${SRC}/functions.js\ - ${SRC}/colors.js\ - ${SRC}/tree/*.js\ - ${SRC}/tree.js\ ${SRC}/rhino.js > ${RHINO} @@echo ${RHINO} built. diff --git a/build.gradle b/build.gradle index e69de29b..fa8efc53 100644 --- a/build.gradle +++ b/build.gradle @@ -0,0 +1,152 @@ +import groovy.io.FileType + +buildscript { + repositories { + mavenCentral() + } + dependencies { + classpath 'com.eriwen:gradle-js-plugin:1.8.0' + } +} + +apply plugin: 'js' + + +repositories { + mavenCentral() +} + +configurations { + rhino +} + +dependencies { + rhino 'org.mozilla:rhino:1.7R4' +} + +project.ext { + rhinoTestSrc = 'out/rhino-test.js' + testSrc = 'test/less' + testOut = 'out/test' +} + +javascript.source { + test { + js { + srcDir '.' + include 'test/rhino/test-header.js' + include 'dist/less-rhino-1.5.0.js' + } + } +} + +combineJs { + source = javascript.source.test.js.files + dest = file(rhinoTestSrc) +} + +task testRhino { + dependsOn 'testRhinoBase', 'testRhinoErrors', 'testRhinoLegacy', 'testRhinoStaticUrls', 'testRhinoCompression' +} + +task testRhinoBase(type: RhinoTest) { + options = [ '--strict-math=true' ] +} + +task testRhinoErrors(type: RhinoTest) { + options = [ '--strict-math=true', '--strict-units=true' ] + testDir = 'errors/' + expectErrors = true +} + +task testRhinoLegacy(type: RhinoTest) { + testDir = 'legacy/' +} + +task testRhinoStaticUrls(type: RhinoTest) { + options = [ '--strict-math=true', '--rootpath=folder (1)/' ] + testDir = 'static-urls/' +} + +task testRhinoCompression(type: RhinoTest) { + options = [ '--compress=true' ] + testDir = 'compression/' +} + +task setupTest { + dependsOn combineJs + doLast { + file(testOut).deleteDir() + } +} + +task cleanupTest << { + file(rhinoTestSrc).delete() + file(testOut).deleteDir() +} + + +class RhinoTest extends DefaultTask { + + RhinoTest() { + dependsOn 'setupTest' + } + + def testDir = '' + def options = [] + def expectErrors = false + + @TaskAction + def runTest() { + int testSuccesses = 0, testFailures = 0, testErrors = 0 + project.file('test/less/' + testDir).eachFileMatch(FileType.FILES, ~/.*\.less/) { lessFile -> + print lessFile + + def out = new java.io.ByteArrayOutputStream() + def execOptions = { + main = 'org.mozilla.javascript.tools.shell.Main' +// main = 'org.mozilla.javascript.tools.debugger.Main' + classpath = project.configurations.rhino + args = [project.rhinoTestSrc, lessFile] + options + standardOutput = out + ignoreExitValue = true + } + try { + def exec = project.javaexec(execOptions) + def actual = out.toString().trim() + def actualResult = project.file(lessFile.path.replace('test/less', project.testOut).replace('.less', '.css')) + project.file(actualResult.parent).mkdirs() + actualResult << actual + def expected + if (expectErrors) { + assert exec.exitValue != 0 + expected = project.file(lessFile.path.replace('.less', '.txt')).text.trim(). + replace('{path}', lessFile.parent + '/'). + replace('{pathhref}', ''). + replace('{404status}', '') + } else { + assert exec.exitValue == 0 + expected = project.file(lessFile.path.replace('.less', '.css').replace('/less/', '/css/')).text.trim() + } + assert actual == expected + testSuccesses++ + println ' ok' + actualResult.delete() + } + catch (ex) { + println ex + println() + testErrors++; + } + catch (AssertionError ae) { + println ' failed' +// println ae + testFailures++ + } + } + println testSuccesses + " ok" + println testFailures + " assertion failed" + println testErrors + " errors" +// assert testFailures + testErrors == 0 + } +} diff --git a/build/rhino-header.js b/build/rhino-header.js index 891f0943..ef81a51b 100644 --- a/build/rhino-header.js +++ b/build/rhino-header.js @@ -1,4 +1,10 @@ if (typeof(window) === 'undefined') { less = {} } else { less = window.less = {} } tree = less.tree = {}; -less.mode = 'rhino'; \ No newline at end of file +less.mode = 'rhino'; + +console = { + log: function(arg) { + java.lang.System.err.println(arg); + } +}; diff --git a/build/rhino-path.js b/build/rhino-path.js index e69de29b..263198f1 100644 --- a/build/rhino-path.js +++ b/build/rhino-path.js @@ -0,0 +1,35 @@ +less.path = { + join: function() { + var parts = []; + for (i in arguments) { + parts = parts.concat(arguments[i].split('/')); + } + var result = []; + for (i in parts) { + var part = parts[i]; + if (part === '..' && result.length > 0) { + result.pop(); + } else if (part === '' && result.length > 0) { + // skip + } else if (part !== '.') { + result.push(part); + } + } + return result.join('/'); + }, + basename: function(p, ext) { + var base = p.split('/').pop(); + if (ext) { + var index = base.lastIndexOf(ext); + if (base.length === index + ext.length) { + base = base.substr(0, index); + } + } + return base; + }, + dirname: function(p) { + var path = p.split('/'); + path.pop(); + return path.join('/'); + } +}; diff --git a/lib/less/functions.js b/lib/less/functions.js index 249aaf17..4408af7b 100644 --- a/lib/less/functions.js +++ b/lib/less/functions.js @@ -428,7 +428,7 @@ tree.functions = { "data-uri": function(mimetypeNode, filePathNode) { - if (typeof window !== 'undefined') { + if (typeof window !== 'undefined' || less.mode == 'rhino') { // TODO rhino implementation return new tree.URL(filePathNode || mimetypeNode, this.currentFileInfo).eval(this.env); } diff --git a/lib/less/rhino.js b/lib/less/rhino.js index 08f31102..1416f750 100644 --- a/lib/less/rhino.js +++ b/lib/less/rhino.js @@ -1,34 +1,63 @@ /*jshint rhino:true, unused: false */ /*global name:true, less, loadStyleSheet */ -var name; -function error(e, filename) { +if (initRhinoTest) { // definition of additional test functions (see rhino/test-header.js) + initRhinoTest(); +} - var content = "Error : " + filename + "\n"; +function formatError(ctx, options) { + options = options || {}; - filename = e.filename || filename; + var message = ""; + var extract = ctx.extract; + var error = []; +// var stylize = options.color ? require('./lessc_helper').stylize : function (str) { return str; }; + var stylize = function (str) { return str; }; - if (e.message) { - content += e.message + "\n"; + // only output a stack if it isn't a less error + if (ctx.stack && !ctx.type) { return stylize(ctx.stack, 'red'); } + + if (!ctx.hasOwnProperty('index') || !extract) { + return ctx.stack || ctx.message; } - var errorline = function (e, i, classname) { - if (e.extract[i]) { - content += - String(parseInt(e.line, 10) + (i - 1)) + - ":" + e.extract[i] + "\n"; + if (typeof(extract[0]) === 'string') { + error.push(stylize((ctx.line - 1) + ' ' + extract[0], 'grey')); + } + + if (typeof(extract[1]) === 'string') { + var errorTxt = ctx.line + ' '; + if (extract[1]) { + errorTxt += extract[1].slice(0, ctx.column) + + stylize(stylize(stylize(extract[1][ctx.column], 'bold') + + extract[1].slice(ctx.column + 1), 'red'), 'inverse'); } - }; - - if (e.stack) { - content += e.stack; - } else if (e.extract) { - content += 'on line ' + e.line + ', column ' + (e.column + 1) + ':\n'; - errorline(e, 0); - errorline(e, 1); - errorline(e, 2); + error.push(errorTxt); } - print(content); + + if (typeof(extract[2]) === 'string') { + error.push(stylize((ctx.line + 1) + ' ' + extract[2], 'grey')); + } + error = error.join('\n') + stylize('', 'reset') + '\n'; + + message += stylize(ctx.type + 'Error: ' + ctx.message, 'red'); + ctx.filename && (message += stylize(' in ', 'red') + ctx.filename + + stylize(' on line ' + ctx.line + ', column ' + (ctx.column + 1) + ':', 'grey')); + + message += '\n' + error; + + if (ctx.callLine) { + message += stylize('from ', 'red') + (ctx.filename || '') + '/n'; + message += stylize(ctx.callLine, 'grey') + ' ' + ctx.callExtract + '/n'; + } + + return message; +} + +function writeError(ctx, options) { + options = options || {}; + if (options.silent) { return; } + print(formatError(ctx, options)); } function loadStyleSheet(sheet, callback, reload, remaining) { @@ -47,16 +76,58 @@ function loadStyleSheet(sheet, callback, reload, remaining) { }); parser.parse(input, function (e, root) { if (e) { - return error(e, sheetName); + return writeError(e); } try { callback(e, root, input, sheet, { local: false, lastModified: 0, remaining: remaining }, sheetName); } catch(e) { - error(e, sheetName); + writeError(e); } }); } +less.Parser.fileLoader = function (originalHref, currentFileInfo, callback, env, newVars) { + + if (currentFileInfo && currentFileInfo.currentDirectory && !/^\//.test(originalHref)) { + originalHref = less.path.join(currentFileInfo.currentDirectory, originalHref); + } + + var href = originalHref; + + var path = less.path.dirname(href); + + var newFileInfo = { + currentDirectory: path + '/', + filename: href + }; + + if (currentFileInfo) { + newFileInfo.entryPath = currentFileInfo.entryPath; + newFileInfo.rootpath = currentFileInfo.rootpath; + newFileInfo.rootFilename = currentFileInfo.rootFilename; + newFileInfo.relativeUrls = currentFileInfo.relativeUrls; + } else { + newFileInfo.entryPath = path; + newFileInfo.rootpath = less.rootpath || path; + newFileInfo.rootFilename = href; + newFileInfo.relativeUrls = env.relativeUrls; + } + + try { + var data = readFile(href); + } catch (e) { + callback({ type: 'File', message: "'" + less.path.basename(href) + "' wasn't found" }); + return; + } + + try { + callback(null, data, href, newFileInfo, { lastModified: 0 }); + } catch (e) { + callback(e, null, href); + } +}; + + function writeFile(filename, content) { var fstream = new java.io.FileWriter(filename); var out = new java.io.BufferedWriter(fstream); @@ -66,53 +137,264 @@ function writeFile(filename, content) { // Command line integration via Rhino (function (args) { - var output, - compress = false, - i, - path; - - for(i = 0; i < args.length; i++) { - switch(args[i]) { - case "-x": - compress = true; + + var options = { + depends: false, + compress: false, + cleancss: false, + max_line_len: -1, + optimization: 1, + silent: false, + verbose: false, + lint: false, + paths: [], + color: true, + strictImports: false, + rootpath: '', + relativeUrls: false, + ieCompat: true, + strictMath: false, + strictUnits: false + }; + var continueProcessing = true, + currentErrorcode; + + var checkArgFunc = function(arg, option) { + if (!option) { + print(arg + " option requires a parameter"); + continueProcessing = false; + return false; + } + return true; + }; + + var checkBooleanArg = function(arg) { + var onOff = /^((on|t|true|y|yes)|(off|f|false|n|no))$/i.exec(arg); + if (!onOff) { + print(" unable to parse "+arg+" as a boolean. use one of on/t/true/y/yes/off/f/false/n/no"); + continueProcessing = false; + return false; + } + return Boolean(onOff[2]); + }; + + var warningMessages = ""; + + args = args.filter(function (arg) { + var match; + + if (match = arg.match(/^-I(.+)$/)) { + options.paths.push(match[1]); + return false; + } + + if (match = arg.match(/^--?([a-z][0-9a-z-]*)(?:=(.*))?$/i)) { arg = match[1] } // was (?:=([^\s]*)), check! + else { return arg } + + switch (arg) { + case 'v': + case 'version': + console.log("lessc " + less.version.join('.') + " (LESS Compiler) [JavaScript]"); + continueProcessing = false; + case 'verbose': + options.verbose = true; + break; + case 's': + case 'silent': + options.silent = true; + break; + case 'l': + case 'lint': + options.lint = true; + break; + case 'strict-imports': + options.strictImports = true; + break; + case 'h': + case 'help': + //TODO +// require('../lib/less/lessc_helper').printUsage(); + continueProcessing = false; + case 'x': + case 'compress': + options.compress = true; + break; + case 'M': + case 'depends': + options.depends = true; + break; + case 'yui-compress': + warningMessages += "yui-compress option has been removed. assuming clean-css."; + options.cleancss = true; + break; + case 'clean-css': + options.cleancss = true; + break; + case 'max-line-len': + if (checkArgFunc(arg, match[2])) { + options.maxLineLen = parseInt(match[2], 10); + if (options.maxLineLen <= 0) { + options.maxLineLen = -1; + } + } + break; + case 'no-color': + options.color = false; + break; + case 'no-ie-compat': + options.ieCompat = false; + break; + case 'no-js': + options.javascriptEnabled = false; + break; + case 'include-path': + if (checkArgFunc(arg, match[2])) { + options.paths = match[2].split(os.type().match(/Windows/) ? ';' : ':') + .map(function(p) { + if (p) { +// return path.resolve(process.cwd(), p); + return p; + } + }); + } + break; + case 'O0': options.optimization = 0; break; + case 'O1': options.optimization = 1; break; + case 'O2': options.optimization = 2; break; + case 'line-numbers': + if (checkArgFunc(arg, match[2])) { + options.dumpLineNumbers = match[2]; + } + break; + case 'source-map': + if (!match[2]) { + options.sourceMap = true; + } else { + options.sourceMap = match[2]; + } + break; + case 'source-map-rootpath': + if (checkArgFunc(arg, match[2])) { + options.sourceMapRootpath = match[2]; + } + break; + case 'source-map-inline': + options.outputSourceFiles = true; + break; + case 'rp': + case 'rootpath': + if (checkArgFunc(arg, match[2])) { + options.rootpath = match[2].replace(/\\/g, '/'); + } + break; + case "ru": + case "relative-urls": + options.relativeUrls = true; + break; + case "sm": + case "strict-math": + if (checkArgFunc(arg, match[2])) { + options.strictMath = checkBooleanArg(match[2]); + } + break; + case "su": + case "strict-units": + if (checkArgFunc(arg, match[2])) { + options.strictUnits = checkBooleanArg(match[2]); + } break; default: - if (!name) { - name = args[i]; - } else if (!output) { - output = args[i]; - } else { - print("unrecognised parameters"); - print("input_file [output_file] [-x]"); - } + console.log('invalid option ' + arg); + continueProcessing = false; + } + }); + + if (!continueProcessing) { + return; + } + + var name = args[0]; + if (name && name != '-') { +// name = path.resolve(process.cwd(), name); + } + var output = args[1]; + var outputbase = args[1]; + if (output) { + options.sourceMapOutputFilename = output; +// output = path.resolve(process.cwd(), output); + if (warningMessages) { + console.log(warningMessages); } } +// options.sourceMapBasepath = process.cwd(); + options.sourceMapBasepath = ''; + + if (options.sourceMap === true) { + if (!output) { + console.log("the sourcemap option only has an optional filename if the css filename is given"); + return; + } + options.sourceMapFullFilename = options.sourceMapOutputFilename + ".map"; + options.sourceMap = less.path.basename(options.sourceMapFullFilename); + } + if (!name) { - print('No files present in the fileset; Check your pattern match in build.xml'); + console.log("lessc: no inout files"); + console.log(""); + // TODO +// require('../lib/less/lessc_helper').printUsage(); + currentErrorcode = 1; + return; + } + +// var ensureDirectory = function (filepath) { +// var dir = path.dirname(filepath), +// cmd, +// existsSync = fs.existsSync || path.existsSync; +// if (!existsSync(dir)) { +// if (mkdirp === undefined) { +// try {mkdirp = require('mkdirp');} +// catch(e) { mkdirp = null; } +// } +// cmd = mkdirp && mkdirp.sync || fs.mkdirSync; +// cmd(dir); +// } +// }; + + if (options.depends) { + if (!outputbase) { + console.log("option --depends requires an output path to be specified"); + return; + } + console.log(outputbase + ": "); + } + + if (!name) { + console.log('No files present in the fileset'); quit(1); } - path = name.split("/");path.pop();path=path.join("/"); var input = readFile(name); if (!input) { - print('lesscss: couldn\'t open file ' + name); + console.log('lesscss: couldn\'t open file ' + name); quit(1); } + options.filename = name; var result; try { - var parser = new less.Parser(); + var parser = new less.Parser(options); parser.parse(input, function (e, root) { if (e) { - error(e, name); + writeError(e, options); quit(1); } else { - result = root.toCSS({compress: compress || false}); + result = root.toCSS(options); if (output) { writeFile(output, result); - print("Written to " + output); + console.log("Written to " + output); } else { print(result); } @@ -121,8 +403,8 @@ function writeFile(filename, content) { }); } catch(e) { - error(e, name); + writeError(e, options); quit(1); } - print("done"); -}(arguments)); \ No newline at end of file + console.log("done"); +}(arguments)); diff --git a/test/less/errors/javascript-error.txt b/test/less/errors/javascript-error.txt index 3c83a966..2c63a0b2 100644 --- a/test/less/errors/javascript-error.txt +++ b/test/less/errors/javascript-error.txt @@ -1,4 +1,4 @@ -SyntaxError: JavaScript evaluation error: 'TypeError: Cannot call method 'toJS' of undefined' in {path}javascript-error.less on line 2, column 27: +SyntaxError: JavaScript evaluation error: 'TypeError: Cannot call method "toJS" of undefined' in {path}javascript-error.less on line 2, column 27: 1 .scope { 2 var: `this.foo.toJS()`; 3 } diff --git a/test/rhino/test-header.js b/test/rhino/test-header.js index e69de29b..124db124 100644 --- a/test/rhino/test-header.js +++ b/test/rhino/test-header.js @@ -0,0 +1,13 @@ +function initRhinoTest() { + process = { title: 'dummy' }; + + less.tree.functions.add = function (a, b) { + return new(less.tree.Dimension)(a.value + b.value); + }; + less.tree.functions.increment = function (a) { + return new(less.tree.Dimension)(a.value + 1); + }; + less.tree.functions._color = function (str) { + if (str.value === "evil red") { return new(less.tree.Color)("600"); } + }; +}