Chromium Code Reviews| OLD | NEW |
|---|---|
| (Empty) | |
| 1 #!/usr/bin/env node | |
| 2 | |
| 3 // Copyright 2017 The Chromium Authors. All rights reserved. | |
| 4 // Use of this source code is governed by a BSD-style license that can be | |
| 5 // found in the LICENSE file. | |
| 6 | |
| 7 'use strict'; | |
| 8 | |
| 9 const os = require('os'); | |
| 10 const fs = require('fs'); | |
| 11 const path = require('path'); | |
| 12 const glob = require('glob'); | |
| 13 const libtidy = require('libtidy'); | |
| 14 const cp = require('child_process'); | |
| 15 const jsdom = require('jsdom'); | |
| 16 const {JSDOM} = jsdom; | |
| 17 | |
| 18 | |
| 19 /** | |
| 20 * Options for sub modules. | |
| 21 */ | |
| 22 const OPTIONS = { | |
| 23 | |
| 24 HTMLTidy: | |
| 25 {'indent': 'yes', 'indent-spaces': '2', 'wrap': '80', 'tidy-mark': 'no'}, | |
|
Raymond Toy
2017/05/11 21:50:28
Haha. I assume you ran clang-format on this file w
hongchan
2017/05/11 22:50:52
:|
| |
| 26 | |
| 27 ClangFormat: ['-style=Chromium', '-assume-filename=a.js'], | |
| 28 | |
| 29 // RegExp text swap collection (ordered key-value pair) for post-processing. | |
| 30 RegExpSwapCollection: [ | |
| 31 // Replace |var| with |let|. | |
| 32 {regexp: /(\n\s{2,}|\()var /, replace: '$1let '}, | |
| 33 | |
| 34 // Move one line up the dangling closing script tags. | |
| 35 {regexp: /\>\n\s{2,}\<\/script\>\n/, replace: '></script>\n'}, | |
| 36 | |
| 37 // Remove all the empty lines in html. | |
| 38 {regexp: /\>\n{2,}/, replace: '>\n'} | |
| 39 ] | |
| 40 }; | |
| 41 | |
| 42 | |
| 43 /** | |
| 44 * Basic utilities. | |
| 45 */ | |
| 46 const Util = { | |
| 47 | |
| 48 logAndExit: (moduleName, messageString) => { | |
| 49 console.error('[layout-test-tidy::' + moduleName + '] ' + messageString); | |
| 50 process.exit(1); | |
| 51 }, | |
| 52 | |
| 53 loadFileToStringSync: (filePath) => { | |
| 54 return fs.readFileSync(filePath); | |
| 55 }, | |
| 56 | |
| 57 writeStringToFileSync: (pageString, filePath) => { | |
| 58 fs.writeFileSync(filePath, pageString); | |
| 59 } | |
| 60 | |
| 61 }; | |
| 62 | |
| 63 | |
| 64 /** | |
| 65 * Wrapper for external modules like HTMLTidy and clang format. | |
| 66 * @type {Object} | |
| 67 */ | |
| 68 const Module = { | |
| 69 | |
| 70 /** | |
| 71 * Perform a batch RegExp string substitution. | |
| 72 * @param {String} targetString Target string. | |
| 73 * @param {Array} swapCollection Array of key-value pairs. Each item is an | |
| 74 * object of { regexp_pattern: replace_string }. | |
| 75 * @return {String} | |
| 76 */ | |
| 77 runRegExpSwapSync: (targetString, regExpSwapCollection) => { | |
| 78 let tempString = targetString; | |
| 79 regExpSwapCollection.forEach((item) => { | |
| 80 let re = new RegExp(item.regexp, 'g'); | |
| 81 tempString = tempString.replace(re, item.replace); | |
| 82 }); | |
| 83 | |
| 84 return tempString; | |
| 85 }, | |
| 86 | |
| 87 /** | |
| 88 * Run HTMLTidy on input string with options. | |
| 89 * @param {String} pageString [description] | |
| 90 * @param {Object} options HTMLTidy option as key-value pair. | |
| 91 * @param {Task} task Associated Task object. | |
| 92 * @return {String} | |
| 93 */ | |
| 94 runHTMLTidySync: (pageString, options, task) => { | |
| 95 let tidyDoc = new libtidy.TidyDoc(); | |
| 96 for (let option in options) | |
| 97 tidyDoc.optSet(option, options[option]); | |
| 98 | |
| 99 // This actually process the data inside of |tidyDoc|. | |
| 100 let logs = ''; | |
| 101 logs += tidyDoc.parseBufferSync(Buffer(pageString)); | |
| 102 logs += tidyDoc.cleanAndRepairSync(); | |
| 103 logs += tidyDoc.runDiagnosticsSync(); | |
| 104 | |
| 105 task.addLog('Module.runHTMLTidySync', logs.split('\n')); | |
| 106 | |
| 107 return tidyDoc.saveBufferSync().toString(); | |
| 108 }, | |
| 109 | |
| 110 /** | |
| 111 * Run clang-format and return a promise. | |
| 112 * @param {String} codeString JS code to apply clang-format. | |
| 113 * @param {Array} clangFormatOption options array for clang-format. | |
| 114 * @param {Task} task Associated Task object. | |
| 115 * @return {Promise} Processed code as string. | |
| 116 * @resolve {String} clang-formatted JS code as string. | |
| 117 * @reject {Error} | |
| 118 */ | |
| 119 runClangFormat: (codeString, clangFormatOption, task) => { | |
| 120 let clangFormatBinary = __dirname + '/node_modules/clang-format/bin/'; | |
| 121 clangFormatBinary += (os.platform() === 'win32') ? | |
| 122 'win32/clang-format.exe' : | |
| 123 os.platform() + '_' + os.arch() + '/clang-format'; | |
| 124 | |
| 125 return new Promise((resolve, reject) => { | |
| 126 // Be sure to pipe the result, not to stdio. | |
|
Raymond Toy
2017/05/11 21:50:28
What does this mean?
hongchan
2017/05/11 22:50:52
Expanded the comment.
| |
| 127 let result = ''; | |
| 128 let clangFormat = cp.spawn( | |
| 129 clangFormatBinary, clangFormatOption, | |
| 130 {stdio: ['pipe', 'pipe', process.stderr]}); | |
| 131 | |
| 132 // Capture the data when it's arrived at the pipe. | |
| 133 clangFormat.stdout.on('data', (data) => { | |
| 134 result += data; | |
| 135 }); | |
| 136 | |
| 137 // For debug purpose: | |
| 138 // clangFormat.stdout.pipe(process.stdout); | |
| 139 | |
| 140 clangFormat.stdout.on('close', (exitCode) => { | |
| 141 if (exitCode) { | |
| 142 Util.logAndExit('Module.runClangFormat', 'exit code = 1'); | |
| 143 } else { | |
| 144 task.addLog('Module.runClangFormat', 'clang-format was successful.'); | |
| 145 resolve(result); | |
| 146 } | |
| 147 }); | |
| 148 | |
| 149 clangFormat.stdin.setEncoding('utf-8'); | |
|
Raymond Toy
2017/05/11 21:50:28
As we mentioned offline, when we read the file, we
hongchan
2017/05/11 22:50:52
Done.
| |
| 150 clangFormat.stdin.write(codeString); | |
| 151 clangFormat.stdin.end(); | |
| 152 }); | |
| 153 }, | |
| 154 | |
| 155 /** | |
| 156 * Detect line overflow and record the line number to the task log. | |
| 157 * @param {String} pageOrCodeString HTML page or JS code data in string. | |
| 158 * @param {TidyTask} task Associated TidyTask object. | |
| 159 */ | |
| 160 detectLineOverflow: (pageOrCodeString, task) => { | |
| 161 let currentLineNumber = 0; | |
| 162 let index0 = 0; | |
| 163 let index1 = 0; | |
| 164 while (index0 < pageOrCodeString.length - 1) { | |
| 165 index1 = pageOrCodeString.indexOf('\n', index0); | |
| 166 if (index1 - index0 > 80) { | |
| 167 task.addLog( | |
| 168 'Module.detectLineOverflow', | |
| 169 'Overflow (> 80 cols.) at line ' + currentLineNumber + '.'); | |
| 170 } | |
| 171 currentLineNumber++; | |
| 172 index0 = index1 + 1; | |
| 173 } | |
| 174 } | |
| 175 | |
| 176 }; | |
| 177 | |
| 178 | |
| 179 /** | |
| 180 * DOM utilities. Process DOM processing after parsing the string by JSDOM. | |
| 181 */ | |
| 182 const DOMUtil = { | |
| 183 | |
| 184 /** | |
| 185 * Parse string, generate JSDOM object and return |document| element. | |
| 186 * @param {String} pageString An HTML page in string. | |
| 187 * @return {Document} A |document| object. | |
| 188 */ | |
| 189 getJSDOMFromStringSync: (pageString) => { | |
| 190 return new JSDOM(`${pageString}`); | |
| 191 // return jsdom_.window.document; | |
| 192 }, | |
| 193 | |
| 194 /** | |
| 195 * In-place tidy up head element. | |
| 196 * @param {Document} document A |document| object. | |
| 197 * @param {Task} task An associated Task object. | |
| 198 * @return {Void} | |
| 199 */ | |
| 200 tidyHeadElementSync: (document, task) => { | |
| 201 try { | |
| 202 // If the title is missing, add one from the file name. | |
| 203 let titleElement = document.querySelector('title'); | |
| 204 if (!titleElement) { | |
| 205 titleElement = document.createElement('title'); | |
| 206 titleElement.textContent = path.basename(task.targetFilePath_); | |
| 207 task.addLog( | |
| 208 'DOMUtil.tidyHeadElementSync', | |
| 209 'Title element was missing thus a new one was added.'); | |
| 210 } | |
| 211 | |
| 212 // The title element should be the first. | |
| 213 let headElement = document.querySelector('head'); | |
| 214 headElement.insertBefore(titleElement, headElement.firstChild); | |
| 215 | |
| 216 // If a script element in body does not have JS code, move to the head | |
| 217 // section. | |
| 218 let scriptElementsInBody = document.body.querySelectorAll('script'); | |
| 219 scriptElementsInBody.forEach((scriptElement) => { | |
| 220 if (!scriptElement.textContent) | |
| 221 headElement.appendChild(scriptElement); | |
| 222 }); | |
| 223 } catch (error) { | |
| 224 task.addLog('DOMUtil.tidyHeadElementSync', error.toString()); | |
| 225 } | |
| 226 }, | |
| 227 | |
| 228 /** | |
| 229 * Sanitize and extract |script| element with JS test code. | |
| 230 * @param {Document} document A |document| object. | |
| 231 * @param {Task} task An associated Task object. | |
| 232 * @return {ScriptElement} | |
| 233 */ | |
| 234 getElementWithTestCodeSync: (document, task) => { | |
| 235 let numberOfElementsWithCode = 0; | |
| 236 let elementWithTestCode; | |
| 237 let scriptElements = document.querySelectorAll('script'); | |
| 238 | |
| 239 scriptElements.forEach(function(element) { | |
|
Raymond Toy
2017/05/11 21:50:28
Not element => { ?
hongchan
2017/05/11 22:50:52
Done. Also renamed variables for clarification.
| |
| 240 // We don't want type attribute. | |
| 241 element.removeAttribute('type'); | |
| 242 | |
| 243 if (element.textContent.length > 0) { | |
| 244 ++numberOfElementsWithCode; | |
| 245 elementWithTestCode = element; | |
| 246 element.id = 'layout-test-code'; | |
| 247 // If the element belongs to something else other than body, move it to | |
| 248 // the body. This fixes script elements that are located in weird | |
| 249 // positions. (e.g outside of body or head) | |
| 250 if (element.parentElement !== document.body) | |
| 251 document.body.appendChild(element); | |
| 252 } | |
| 253 }); | |
| 254 | |
| 255 if (numberOfElementsWithCode !== 1) { | |
| 256 task.addLog( | |
| 257 'DOMUtil.getElementWithTestCodeSync', | |
| 258 numberOfElementsWithCode + ' <script> element(s) with JS ' + | |
| 259 'code were found.'); | |
| 260 elementWithTestCode = null; | |
| 261 } | |
| 262 | |
| 263 return elementWithTestCode; | |
| 264 } | |
| 265 | |
| 266 }; | |
| 267 | |
| 268 | |
| 269 /** | |
| 270 * @class TidyTask | |
| 271 * @description Per-file processing task. This object should be constructed | |
| 272 * directly. The task runner creates this when it is necessary. | |
| 273 */ | |
| 274 class TidyTask { | |
| 275 /** | |
| 276 * @param {String} targetFilePath A path to file to be processed. | |
| 277 * @param {Object} options Task options. | |
| 278 * @param {Boolean} options.inplace |true| for in-place processing directly | |
| 279 * writing into the target file. By default, | |
| 280 * this is |false| and the result is piped | |
| 281 * into the stdout. | |
| 282 * @param {Boolean} options.verbose Prints out warnings and logs from the | |
| 283 * process when |true|. |false| by default. | |
| 284 */ | |
| 285 constructor(targetFilePath, options) { | |
| 286 this.targetFilePath_ = targetFilePath; | |
| 287 this.options_ = options; | |
| 288 | |
| 289 this.fileType_ = path.extname(this.targetFilePath_); | |
| 290 this.pageString_ = Util.loadFileToStringSync(this.targetFilePath_); | |
| 291 this.jsdom_ = null; | |
| 292 this.logs_ = {}; | |
| 293 } | |
| 294 | |
| 295 /** | |
| 296 * Run processing sequence. Don't call this directly. | |
| 297 * @param {Function} taskDone Task runner callback function. | |
| 298 */ | |
| 299 run(taskDone) { | |
| 300 switch (this.fileType_) { | |
| 301 case '.html': | |
| 302 this.processHTML_(taskDone); | |
| 303 break; | |
| 304 case '.js': | |
| 305 this.processJS_(taskDone); | |
| 306 break; | |
| 307 default: | |
| 308 Util.logAndExit( | |
| 309 'TidyTask.constructor', 'Invalid file type: ' + this.fileType_); | |
| 310 break; | |
| 311 } | |
| 312 } | |
| 313 | |
| 314 /** | |
| 315 * Process HTML file. The processing performs the following in order: | |
| 316 * - DOM parsing to sanitize invalid/incorrect markup structure. | |
| 317 * - Extract JS code, apply clang-format and inject the code to element. | |
| 318 * - Apply HTMLTidy to the markup. | |
| 319 * - RegExp substitution. | |
| 320 * - Detect any line overflows 80 columns. | |
| 321 * @param {Function} taskDone completion callback. | |
| 322 */ | |
| 323 processHTML_(taskDone) { | |
| 324 // Parse page string into JSDOM.element object. | |
| 325 this.jsdom_ = DOMUtil.getJSDOMFromStringSync(this.pageString_); | |
| 326 | |
| 327 // Clean up the head element section. | |
| 328 DOMUtil.tidyHeadElementSync(this.jsdom_.window.document, this); | |
| 329 | |
| 330 let scriptElement = | |
| 331 DOMUtil.getElementWithTestCodeSync(this.jsdom_.window.document, this); | |
| 332 | |
| 333 if (!scriptElement) | |
| 334 Util.logAndExit('TidyTask.processHTML_', 'Invalid <script> element.'); | |
| 335 | |
| 336 // Extract JS code string with additional braces for indentation hack. | |
| 337 let codeString = '{{{/*start*/' + scriptElement.textContent + '/*end*/}}}'; | |
| 338 | |
| 339 // Start with clang-foramt, then HTMLTidy and RegExp substitution. | |
| 340 Module.runClangFormat(codeString, OPTIONS.ClangFormat, this) | |
| 341 .then((formattedCodeString) => { | |
| 342 // Remove extra braces for the indentation hack. | |
| 343 formattedCodeString = formattedCodeString.substring( | |
| 344 formattedCodeString.indexOf('/*start*/') + 10); | |
| 345 formattedCodeString = formattedCodeString.substring( | |
| 346 0, formattedCodeString.indexOf('/*end*/')); | |
|
Raymond Toy
2017/05/11 21:50:28
This is not very robust. What if the code actuall
hongchan
2017/05/11 22:50:52
Done.
| |
| 347 | |
| 348 // Replace the original code with clang-formatted code. | |
| 349 scriptElement.textContent = formattedCodeString; | |
| 350 | |
| 351 // Then tidy the text data from JSDOM. After this point, DOM | |
| 352 // manipulation is not possible anymore. | |
| 353 let pageString = this.jsdom_.serialize(); | |
| 354 pageString = | |
| 355 Module.runHTMLTidySync(pageString, OPTIONS.HTMLTidy, this); | |
| 356 pageString = Module.runRegExpSwapSync( | |
| 357 pageString, OPTIONS.RegExpSwapCollection); | |
| 358 | |
| 359 // Detect any line goes over column 80. | |
| 360 Module.detectLineOverflow(pageString, this); | |
| 361 | |
| 362 this.finish_(pageString, taskDone); | |
| 363 }); | |
| 364 } | |
| 365 | |
| 366 /** | |
| 367 * Process JS file. The processing performs the following in order: | |
| 368 * - Extract JS code, apply clang-format and inject the code to element. | |
| 369 * - RegExp substitution. | |
| 370 * - Detect any line overflows 80 columns. | |
| 371 * @param {Function} taskDone completion callback. | |
| 372 */ | |
| 373 processJS_(taskDone) { | |
| 374 // The file is a JS code: run clang-format, RegExp substitution and check | |
| 375 // for overflowed lines. | |
| 376 Module.runClangFormat(this.pageString_, OPTIONS.ClangFormat, this) | |
| 377 .then((formattedCodeString) => { | |
| 378 formattedCodeString = Module.runRegExpSwapSync( | |
| 379 formattedCodeString, [OPTIONS.RegExpSwapCollection[0]]); | |
| 380 Module.detectLineOverflow(formattedCodeString, this); | |
| 381 this.finish_(formattedCodeString, taskDone); | |
| 382 }); | |
| 383 } | |
| 384 | |
| 385 finish_(resultString, taskDone) { | |
| 386 if (this.options_.inplace) { | |
| 387 Util.writeStringToFileSync(resultString, this.targetFilePath_); | |
| 388 } else { | |
| 389 process.stdout.write(resultString); | |
| 390 } | |
| 391 | |
| 392 this.printLog(); | |
| 393 taskDone(); | |
| 394 } | |
| 395 | |
| 396 /** | |
| 397 * Adding log message. | |
| 398 * @param {String} location Caller information. | |
| 399 * @param {String} message Log message. | |
| 400 */ | |
| 401 addLog(location, message) { | |
| 402 if (!this.logs_.hasOwnProperty(location)) | |
| 403 this.logs_[location] = []; | |
| 404 this.logs_[location].push(message); | |
| 405 } | |
| 406 | |
| 407 /** | |
| 408 * Print log messages at the end of task. | |
| 409 */ | |
| 410 printLog() { | |
| 411 if (!this.options_.verbose) | |
| 412 return; | |
| 413 | |
| 414 console.log('> Logs from: ' + this.targetFilePath_); | |
| 415 for (let location in this.logs_) { | |
| 416 console.log(' [] ' + location); | |
| 417 this.logs_[location].forEach((message) => { | |
| 418 if (Array.isArray(message)) { | |
| 419 message.forEach((subMessage) => { | |
| 420 if (subMessage.length > 0) | |
| 421 console.log(' - ' + subMessage); | |
| 422 }); | |
| 423 } else { | |
| 424 console.log(' - ' + message); | |
| 425 } | |
| 426 }); | |
| 427 } | |
| 428 } | |
| 429 } | |
| 430 | |
| 431 | |
| 432 /** | |
| 433 * @class TidyTaskRunner | |
| 434 */ | |
| 435 class TidyTaskRunner { | |
| 436 /** | |
| 437 * @param {Array} files A list of file paths. | |
| 438 * @param {Object} options Task options. | |
| 439 * @param {Boolean} options.inplace |true| for in-place processing directly | |
| 440 * writing into the target file. By default, | |
| 441 * this is |false| and the result is piped | |
| 442 * into the stdout. | |
| 443 * @param {Boolean} options.verbose Prints out warnings and logs from the | |
| 444 * process when |true|. |false| by default. | |
| 445 * @return {TidyTaskRunner} A task runner object. | |
| 446 */ | |
| 447 constructor(files, options) { | |
| 448 this.targetFiles_ = files; | |
| 449 this.options_ = options; | |
| 450 this.tasks_ = []; | |
| 451 this.currentTask_ = 0; | |
| 452 } | |
| 453 | |
| 454 startProcessing() { | |
| 455 this.targetFiles_.forEach((filePath) => { | |
| 456 this.tasks_.push(new TidyTask(filePath, this.options_)); | |
| 457 }); | |
| 458 this.log_('Task runner started: ' + this.targetFiles_.length + ' file(s).'); | |
| 459 this.runTask_(); | |
| 460 } | |
| 461 | |
| 462 runTask_() { | |
| 463 this.log_( | |
| 464 'Running task #' + (this.currentTask_ + 1) + ': ' + | |
| 465 this.targetFiles_[this.currentTask_] + | |
| 466 (this.options_.inplace ? ' (IN-PLACE)' : '')); | |
| 467 this.tasks_[this.currentTask_].run(this.done_.bind(this)); | |
| 468 } | |
| 469 | |
| 470 done_() { | |
| 471 this.log_('Task #' + (this.currentTask_ + 1) + ' completed.'); | |
| 472 this.currentTask_++; | |
| 473 if (this.currentTask_ < this.tasks_.length) { | |
| 474 this.runTask_(); | |
| 475 } else { | |
| 476 this.log_( | |
| 477 'Task runner completed: ' + this.targetFiles_.length + | |
| 478 ' file(s) processed.'); | |
| 479 } | |
| 480 } | |
| 481 | |
| 482 log_(message) { | |
| 483 if (this.options_.verbose) | |
| 484 console.log('[layout-test-tidy] ' + message); | |
| 485 } | |
| 486 } | |
| 487 | |
| 488 | |
| 489 // Entry point. | |
| 490 function main() { | |
| 491 let args = process.argv.slice(2); | |
| 492 | |
| 493 // Extract options from the arguments. | |
| 494 let optionArgs = args.filter((arg, index) => { | |
| 495 if (arg.startsWith('-') || arg.startsWith('--')) { | |
| 496 args[index] = null; | |
| 497 return true; | |
| 498 } | |
| 499 }); | |
| 500 | |
| 501 args = args.filter(arg => arg); | |
| 502 | |
| 503 // A single target (a file or a directory) is allowed. | |
| 504 if (args.length !== 1) { | |
| 505 Util.logAndExit('main', 'Please specify a single target. (' + args + ')'); | |
| 506 } | |
| 507 | |
| 508 // Populate options flags. | |
| 509 let options = { | |
| 510 inPlace: optionArgs.includes('-i') || optionArgs.includes('--in-place'), | |
| 511 verbose: optionArgs.includes('-v') || optionArgs.includes('--verbose') | |
| 512 }; | |
| 513 | |
| 514 // Collect target file(s) from the file system. | |
| 515 let targetPath = args[0]; | |
| 516 let files = []; | |
| 517 if (targetPath) { | |
| 518 try { | |
| 519 let stat = fs.lstatSync(targetPath); | |
| 520 if (stat.isFile()) { | |
| 521 files.push(targetPath); | |
| 522 } else if (stat.isDirectory()) { | |
| 523 files = glob.sync( | |
| 524 targetPath + '/**/*.{html,js}', {ignore: ['**/node_modules/**/*']}); | |
| 525 } | |
| 526 } catch (error) { | |
| 527 let errorMessage = 'Invalid file path. (' + targetPath + ')\n' + | |
| 528 ' > ' + error.toString(); | |
| 529 Util.logAndExit('main', errorMessage); | |
| 530 } | |
| 531 } | |
| 532 | |
| 533 // Files to be skipped. | |
| 534 let filesToBeSkipped = | |
| 535 Util.loadFileToStringSync('skip-tidy').toString().split('\n'); | |
| 536 filesToBeSkipped.forEach((fileSkipped) => { | |
| 537 let index = files.indexOf(fileSkipped); | |
| 538 if (index > -1) { | |
| 539 files.splice(index, 1); | |
| 540 } | |
| 541 }); | |
| 542 | |
| 543 if (files.length > 0) { | |
| 544 let taskRunner = new TidyTaskRunner(files, options); | |
| 545 taskRunner.startProcessing(); | |
| 546 } else { | |
| 547 Util.logAndExit('main', 'No files to process.'); | |
| 548 } | |
| 549 } | |
| 550 | |
| 551 main(); | |
| OLD | NEW |