Chromium Code Reviews| Index: third_party/WebKit/LayoutTests/webaudio/tools/layout-test-tidy.js |
| diff --git a/third_party/WebKit/LayoutTests/webaudio/tools/layout-test-tidy.js b/third_party/WebKit/LayoutTests/webaudio/tools/layout-test-tidy.js |
| new file mode 100644 |
| index 0000000000000000000000000000000000000000..bffab4bc075da8410e10cce0e191affa68bd685e |
| --- /dev/null |
| +++ b/third_party/WebKit/LayoutTests/webaudio/tools/layout-test-tidy.js |
| @@ -0,0 +1,562 @@ |
| +#!/usr/bin/env node |
| + |
| +// Copyright 2017 The Chromium Authors. All rights reserved. |
| +// Use of this source code is governed by a BSD-style license that can be |
| +// found in the LICENSE file. |
| + |
| +'use strict'; |
| + |
| +const os = require('os'); |
| +const fs = require('fs'); |
| +const path = require('path'); |
| +const glob = require('glob'); |
| +const libtidy = require('libtidy'); |
| +const cp = require('child_process'); |
| +const jsdom = require('jsdom'); |
| +const {JSDOM} = jsdom; |
| + |
| + |
| +/** |
| + * Options for sub modules. |
| + */ |
| +const OPTIONS = { |
| + |
| + HTMLTidy: |
| + {'indent': 'yes', 'indent-spaces': '2', 'wrap': '80', 'tidy-mark': 'no'}, |
| + |
| + ClangFormat: ['-style=Chromium', '-assume-filename=a.js'], |
| + |
| + // RegExp text swap collection (ordered key-value pair) for post-processing. |
| + RegExpSwapCollection: [ |
| + // Replace |var| with |let|. |
| + {regexp: /(\n\s{2,}|\()var /, replace: '$1let '}, |
| + |
| + // Move one line up the dangling closing script tags. |
| + {regexp: /\>\n\s{2,}\<\/script\>\n/, replace: '></script>\n'}, |
| + |
| + // Remove all the empty lines in html. |
| + {regexp: /\>\n{2,}/, replace: '>\n'} |
| + ] |
| +}; |
| + |
| + |
| +/** |
| + * Basic utilities. |
| + */ |
| +const Util = { |
| + |
| + logAndExit: (moduleName, messageString) => { |
| + console.error('[layout-test-tidy::' + moduleName + '] ' + messageString); |
| + process.exit(1); |
| + }, |
| + |
| + loadFileToStringSync: (filePath) => { |
| + return fs.readFileSync(filePath, 'utf8').toString(); |
| + }, |
| + |
| + writeStringToFileSync: (pageString, filePath) => { |
| + fs.writeFileSync(filePath, pageString); |
| + } |
| + |
| +}; |
| + |
| + |
| +/** |
| + * Wrapper for external modules like HTMLTidy and clang format. |
| + * @type {Object} |
| + */ |
| +const Module = { |
| + |
| + /** |
| + * Perform a batch RegExp string substitution. |
| + * @param {String} targetString Target string. |
| + * @param {Array} swapCollection Array of key-value pairs. Each item is an |
| + * object of { regexp_pattern: replace_string }. |
| + * @return {String} |
| + */ |
| + runRegExpSwapSync: (targetString, regExpSwapCollection) => { |
| + let tempString = targetString; |
| + regExpSwapCollection.forEach((item) => { |
| + let re = new RegExp(item.regexp, 'g'); |
| + tempString = tempString.replace(re, item.replace); |
| + }); |
| + |
| + return tempString; |
| + }, |
| + |
| + /** |
| + * Run HTMLTidy on input string with options. |
| + * @param {String} pageString [description] |
| + * @param {Object} options HTMLTidy option as key-value pair. |
| + * @param {Task} task Associated Task object. |
| + * @return {String} |
| + */ |
| + runHTMLTidySync: (pageString, options, task) => { |
| + let tidyDoc = new libtidy.TidyDoc(); |
| + for (let option in options) |
| + tidyDoc.optSet(option, options[option]); |
| + |
| + // This actually process the data inside of |tidyDoc|. |
| + let logs = ''; |
| + logs += tidyDoc.parseBufferSync(Buffer(pageString)); |
| + logs += tidyDoc.cleanAndRepairSync(); |
| + logs += tidyDoc.runDiagnosticsSync(); |
| + |
| + task.addLog('Module.runHTMLTidySync', logs.split('\n')); |
| + |
| + return tidyDoc.saveBufferSync().toString(); |
| + }, |
| + |
| + /** |
| + * Run clang-format and return a promise. |
| + * @param {String} codeString JS code to apply clang-format. |
| + * @param {Array} clangFormatOption options array for clang-format. |
| + * @param {Number} indentLevel Code indentation level. |
| + * @param {Task} task Associated Task object. |
| + * @return {Promise} Processed code as string. |
| + * @resolve {String} clang-formatted JS code as string. |
| + * @reject {Error} |
| + */ |
| + runClangFormat: (codeString, clangFormatOption, indentLevel, task) => { |
| + let clangFormatBinary = __dirname + '/node_modules/clang-format/bin/'; |
| + clangFormatBinary += (os.platform() === 'win32') ? |
| + 'win32/clang-format.exe' : |
| + os.platform() + '_' + os.arch() + '/clang-format'; |
| + |
| + if (indentLevel > 0) { |
| + codeString = |
| + '{'.repeat(indentLevel) + codeString + '}'.repeat(indentLevel); |
| + } |
| + |
| + return new Promise((resolve, reject) => { |
| + // Be sure to pipe the result to the child process, not to this process's |
| + // stdout. |
| + let result = ''; |
| + let clangFormat = cp.spawn( |
| + clangFormatBinary, clangFormatOption, |
| + {stdio: ['pipe', 'pipe', process.stderr]}); |
| + |
| + // Capture the data when it's arrived at the pipe. |
| + clangFormat.stdout.on('data', (data) => { |
| + result += data; |
| + }); |
| + |
| + // For debug purpose: |
| + // clangFormat.stdout.pipe(process.stdout); |
| + |
| + clangFormat.stdout.on('close', (exitCode) => { |
| + if (exitCode) { |
| + Util.logAndExit('Module.runClangFormat', 'exit code = 1'); |
| + } else { |
| + task.addLog('Module.runClangFormat', 'clang-format was successful.'); |
| + |
| + // Remove shim braces for indentation hack. |
| + if (indentLevel > 0) { |
| + let codeStart = 0; |
| + let codeEnd = result.length - 1; |
| + for (let i = 0; i < indentLevel; ++i) { |
| + codeStart = result.indexOf('\n', codeStart + 1); |
| + codeEnd = result.lastIndexOf('\n', codeEnd - 1); |
| + } |
| + result = result.substring(codeStart + 1, codeEnd); |
| + } |
| + |
| + resolve(result); |
| + } |
| + }); |
| + |
| + clangFormat.stdin.setEncoding('utf-8'); |
| + clangFormat.stdin.write(codeString); |
| + clangFormat.stdin.end(); |
| + }); |
| + }, |
| + |
| + /** |
| + * Detect line overflow and record the line number to the task log. |
| + * @param {String} pageOrCodeString HTML page or JS code data in string. |
| + * @param {TidyTask} task Associated TidyTask object. |
| + */ |
| + detectLineOverflow: (pageOrCodeString, task) => { |
| + let currentLineNumber = 0; |
| + let index0 = 0; |
| + let index1 = 0; |
| + while (index0 < pageOrCodeString.length - 1) { |
| + index1 = pageOrCodeString.indexOf('\n', index0); |
| + if (index1 - index0 > 80) { |
| + task.addLog( |
| + 'Module.detectLineOverflow', |
| + 'Overflow (> 80 cols.) at line ' + currentLineNumber + '.'); |
| + } |
| + currentLineNumber++; |
| + index0 = index1 + 1; |
| + } |
| + } |
| + |
| +}; |
| + |
| + |
| +/** |
| + * DOM utilities. Process DOM processing after parsing the string by JSDOM. |
| + */ |
| +const DOMUtil = { |
| + |
| + /** |
| + * Parse string, generate JSDOM object and return |document| element. |
| + * @param {String} pageString An HTML page in string. |
| + * @return {Document} A |document| object. |
| + */ |
| + getJSDOMFromStringSync: (pageString) => { |
| + return new JSDOM(`${pageString}`); |
| + // return jsdom_.window.document; |
| + }, |
| + |
| + /** |
| + * In-place tidy up head element. |
| + * @param {Document} document A |document| object. |
| + * @param {Task} task An associated Task object. |
| + * @return {Void} |
| + */ |
| + tidyHeadElementSync: (document, task) => { |
| + try { |
| + // If the title is missing, add one from the file name. |
| + let titleElement = document.querySelector('title'); |
| + if (!titleElement) { |
| + titleElement = document.createElement('title'); |
| + titleElement.textContent = path.basename(task.targetFilePath_); |
| + task.addLog( |
| + 'DOMUtil.tidyHeadElementSync', |
| + 'Title element was missing thus a new one was added.'); |
| + } |
| + |
| + // The title element should be the first. |
| + let headElement = document.querySelector('head'); |
| + headElement.insertBefore(titleElement, headElement.firstChild); |
| + |
| + // If a script element in body does not have JS code, move to the head |
| + // section. |
| + let scriptElementsInBody = document.body.querySelectorAll('script'); |
| + scriptElementsInBody.forEach((scriptElement) => { |
| + if (!scriptElement.textContent) |
| + headElement.appendChild(scriptElement); |
| + }); |
| + } catch (error) { |
| + task.addLog('DOMUtil.tidyHeadElementSync', error.toString()); |
| + } |
| + }, |
| + |
| + /** |
| + * Sanitize and extract |script| element with JS test code. |
| + * @param {Document} document A |document| object. |
| + * @param {Task} task An associated Task object. |
| + * @return {ScriptElement} |
| + */ |
| + getElementWithTestCodeSync: (document, task) => { |
| + let numberOfScriptElementsWithCode = 0; |
| + let scriptElementWithTestCode; |
| + let scriptElements = document.querySelectorAll('script'); |
| + |
| + scriptElements.forEach((scriptElement) => { |
| + // We don't want type attribute. |
| + scriptElement.removeAttribute('type'); |
| + |
| + if (scriptElement.textContent.length > 0) { |
| + ++numberOfScriptElementsWithCode; |
| + scriptElementWithTestCode = scriptElement; |
| + scriptElement.id = 'layout-test-code'; |
| + // If the element belongs to something else other than body, move it to |
| + // the body. This fixes script elements that are located in weird |
| + // positions. (e.g outside of body or head) |
| + if (scriptElement.parentElement !== document.body) |
| + document.body.appendChild(scriptElement); |
| + } |
| + }); |
| + |
| + if (numberOfScriptElementsWithCode !== 1) { |
|
Raymond Toy
2017/05/12 16:19:01
I think I'd add a comment here that says it's perf
|
| + task.addLog( |
| + 'DOMUtil.getElementWithTestCodeSync', |
| + numberOfScriptElementsWithCode + ' <script> element(s) with JS ' + |
| + 'code were found.'); |
| + scriptElementWithTestCode = null; |
| + } |
| + |
| + return scriptElementWithTestCode; |
| + } |
| + |
| +}; |
| + |
| + |
| +/** |
| + * @class TidyTask |
| + * @description Per-file processing task. This object should be constructed |
| + * directly. The task runner creates this when it is necessary. |
| + */ |
| +class TidyTask { |
| + /** |
| + * @param {String} targetFilePath A path to file to be processed. |
| + * @param {Object} options Task options. |
| + * @param {Boolean} options.inplace |true| for in-place processing directly |
| + * writing into the target file. By default, |
| + * this is |false| and the result is piped |
| + * into the stdout. |
| + * @param {Boolean} options.verbose Prints out warnings and logs from the |
| + * process when |true|. |false| by default. |
| + */ |
| + constructor(targetFilePath, options) { |
| + this.targetFilePath_ = targetFilePath; |
| + this.options_ = options; |
| + |
| + this.fileType_ = path.extname(this.targetFilePath_); |
| + this.pageString_ = Util.loadFileToStringSync(this.targetFilePath_); |
| + this.jsdom_ = null; |
| + this.logs_ = {}; |
| + } |
| + |
| + /** |
| + * Run processing sequence. Don't call this directly. |
| + * @param {Function} taskDone Task runner callback function. |
| + */ |
| + run(taskDone) { |
| + switch (this.fileType_) { |
| + case '.html': |
| + this.processHTML_(taskDone); |
| + break; |
| + case '.js': |
| + this.processJS_(taskDone); |
| + break; |
| + default: |
| + Util.logAndExit( |
| + 'TidyTask.constructor', 'Invalid file type: ' + this.fileType_); |
| + break; |
| + } |
| + } |
| + |
| + /** |
| + * Process HTML file. The processing performs the following in order: |
| + * - DOM parsing to sanitize invalid/incorrect markup structure. |
| + * - Extract JS code, apply clang-format and inject the code to element. |
| + * - Apply HTMLTidy to the markup. |
| + * - RegExp substitution. |
| + * - Detect any line overflows 80 columns. |
| + * @param {Function} taskDone completion callback. |
| + */ |
| + processHTML_(taskDone) { |
| + // Parse page string into JSDOM.element object. |
| + this.jsdom_ = DOMUtil.getJSDOMFromStringSync(this.pageString_); |
| + |
| + // Clean up the head element section. |
| + DOMUtil.tidyHeadElementSync(this.jsdom_.window.document, this); |
| + |
| + let scriptElement = |
| + DOMUtil.getElementWithTestCodeSync(this.jsdom_.window.document, this); |
| + |
| + if (!scriptElement) |
| + Util.logAndExit('TidyTask.processHTML_', 'Invalid <script> element.'); |
| + |
| + // Start with clang-foramt, then HTMLTidy and RegExp substitution. |
| + Module.runClangFormat( |
| + scriptElement.textContent, OPTIONS.ClangFormat, 3, this) |
| + .then((formattedCodeString) => { |
| + // Replace the original code with clang-formatted code. |
| + scriptElement.textContent = formattedCodeString; |
| + |
| + // Then tidy the text data from JSDOM. After this point, DOM |
| + // manipulation is not possible anymore. |
| + let pageString = this.jsdom_.serialize(); |
| + pageString = |
| + Module.runHTMLTidySync(pageString, OPTIONS.HTMLTidy, this); |
| + pageString = Module.runRegExpSwapSync( |
| + pageString, OPTIONS.RegExpSwapCollection); |
| + |
| + // Detect any line goes over column 80. |
| + Module.detectLineOverflow(pageString, this); |
| + |
| + this.finish_(pageString, taskDone); |
| + }); |
| + } |
| + |
| + /** |
| + * Process JS file. The processing performs the following in order: |
| + * - Extract JS code, apply clang-format and inject the code to element. |
| + * - RegExp substitution. |
| + * - Detect any line overflows 80 columns. |
| + * @param {Function} taskDone completion callback. |
| + */ |
| + processJS_(taskDone) { |
| + // The file is a JS code: run clang-format, RegExp substitution and check |
| + // for overflowed lines. |
| + Module.runClangFormat(this.pageString_, OPTIONS.ClangFormat, 0, this) |
| + .then((formattedCodeString) => { |
| + formattedCodeString = Module.runRegExpSwapSync( |
| + formattedCodeString, [OPTIONS.RegExpSwapCollection[0]]); |
| + Module.detectLineOverflow(formattedCodeString, this); |
| + this.finish_(formattedCodeString, taskDone); |
| + }); |
| + } |
| + |
| + finish_(resultString, taskDone) { |
| + if (this.options_.inplace) { |
| + Util.writeStringToFileSync(resultString, this.targetFilePath_); |
| + } else { |
| + process.stdout.write(resultString); |
| + } |
| + |
| + this.printLog(); |
| + taskDone(); |
| + } |
| + |
| + /** |
| + * Adding log message. |
| + * @param {String} location Caller information. |
| + * @param {String} message Log message. |
| + */ |
| + addLog(location, message) { |
| + if (!this.logs_.hasOwnProperty(location)) |
| + this.logs_[location] = []; |
| + this.logs_[location].push(message); |
| + } |
| + |
| + /** |
| + * Print log messages at the end of task. |
| + */ |
| + printLog() { |
| + if (!this.options_.verbose) |
| + return; |
| + |
| + console.warn('> Logs from: ' + this.targetFilePath_); |
| + for (let location in this.logs_) { |
| + console.warn(' [] ' + location); |
| + this.logs_[location].forEach((message) => { |
| + if (Array.isArray(message)) { |
| + message.forEach((subMessage) => { |
| + if (subMessage.length > 0) |
| + console.warn(' - ' + subMessage); |
| + }); |
| + } else { |
| + console.warn(' - ' + message); |
| + } |
| + }); |
| + } |
| + } |
| +} |
| + |
| + |
| +/** |
| + * @class TidyTaskRunner |
| + */ |
| +class TidyTaskRunner { |
| + /** |
| + * @param {Array} files A list of file paths. |
| + * @param {Object} options Task options. |
| + * @param {Boolean} options.inplace |true| for in-place processing directly |
| + * writing into the target file. By default, |
| + * this is |false| and the result is piped |
| + * into the stdout. |
| + * @param {Boolean} options.verbose Prints out warnings and logs from the |
| + * process when |true|. |false| by default. |
| + * @return {TidyTaskRunner} A task runner object. |
| + */ |
| + constructor(files, options) { |
| + this.targetFiles_ = files; |
| + this.options_ = options; |
| + this.tasks_ = []; |
| + this.currentTask_ = 0; |
| + } |
| + |
| + startProcessing() { |
| + this.targetFiles_.forEach((filePath) => { |
| + this.tasks_.push(new TidyTask(filePath, this.options_)); |
| + }); |
| + this.log_('Task runner started: ' + this.targetFiles_.length + ' file(s).'); |
| + this.runTask_(); |
| + } |
| + |
| + runTask_() { |
| + this.log_( |
| + 'Running task #' + (this.currentTask_ + 1) + ': ' + |
| + this.targetFiles_[this.currentTask_] + |
| + (this.options_.inplace ? ' (IN-PLACE)' : '')); |
| + this.tasks_[this.currentTask_].run(this.done_.bind(this)); |
| + } |
| + |
| + done_() { |
| + this.log_('Task #' + (this.currentTask_ + 1) + ' completed.'); |
| + this.currentTask_++; |
| + if (this.currentTask_ < this.tasks_.length) { |
| + this.runTask_(); |
| + } else { |
| + this.log_( |
| + 'Task runner completed: ' + this.targetFiles_.length + |
| + ' file(s) processed.'); |
| + } |
| + } |
| + |
| + log_(message) { |
| + if (this.options_.verbose) |
| + console.warn('[layout-test-tidy] ' + message); |
| + } |
| +} |
| + |
| + |
| +// Entry point. |
| +function main() { |
| + let args = process.argv.slice(2); |
| + |
| + // Extract options from the arguments. |
| + let optionArgs = args.filter((arg, index) => { |
| + if (arg.startsWith('-') || arg.startsWith('--')) { |
| + args[index] = null; |
| + return true; |
| + } |
| + }); |
| + |
| + args = args.filter(arg => arg); |
| + |
| + // A single target (a file or a directory) is allowed. |
| + if (args.length !== 1) { |
| + Util.logAndExit('main', 'Please specify a single target. (' + args + ')'); |
| + } |
| + |
| + // Populate options flags. |
| + let options = { |
| + inPlace: optionArgs.includes('-i') || optionArgs.includes('--in-place'), |
| + verbose: optionArgs.includes('-v') || optionArgs.includes('--verbose') |
| + }; |
| + |
| + // Collect target file(s) from the file system. |
| + let targetPath = args[0]; |
| + let files = []; |
| + if (targetPath) { |
| + try { |
| + let stat = fs.lstatSync(targetPath); |
| + if (stat.isFile()) { |
| + files.push(targetPath); |
| + } else if (stat.isDirectory()) { |
| + files = glob.sync( |
| + targetPath + '/**/*.{html,js}', {ignore: ['**/node_modules/**/*']}); |
| + } |
| + } catch (error) { |
| + let errorMessage = 'Invalid file path. (' + targetPath + ')\n' + |
| + ' > ' + error.toString(); |
| + Util.logAndExit('main', errorMessage); |
| + } |
| + } |
| + |
| + // Files to be skipped. |
| + let filesToBeSkipped = |
| + Util.loadFileToStringSync('skip-tidy').toString().split('\n'); |
| + filesToBeSkipped.forEach((fileSkipped) => { |
| + let index = files.indexOf(fileSkipped); |
| + if (index > -1) { |
| + files.splice(index, 1); |
| + } |
| + }); |
| + |
| + if (files.length > 0) { |
| + let taskRunner = new TidyTaskRunner(files, options); |
| + taskRunner.startProcessing(); |
| + } else { |
| + Util.logAndExit('main', 'No files to process.'); |
| + } |
| +} |
| + |
| +main(); |