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', | |
| 26 'indent-spaces': '2', | |
| 27 'wrap': '80', | |
| 28 'tidy-mark': 'no' | |
| 29 }, | |
| 30 | |
| 31 ClangFormat: [ | |
| 32 '-style=Chromium', | |
| 33 '-assume-filename=a.js' | |
| 34 ], | |
| 35 | |
| 36 // RegExp text swap collection (ordered key-value pair) for post-processing. | |
| 37 RegExpSwapCollection: [ | |
| 38 {regexp: /var /, replace: 'let '}, | |
|
Raymond Toy
2017/05/10 21:28:44
I think you need to tighten up the regexp here:
l
hongchan
2017/05/11 21:13:02
Please advise if you have a better idea.
| |
| 39 // Move one line up the dangling closing script tags. | |
| 40 {regexp: /\>\n\s{2,}\<\/script\>\n/, replace: '></script>\n'}, | |
| 41 // Clean up redundant braces at the beginning/end. | |
|
Raymond Toy
2017/05/10 21:28:44
Expand on why this is needed. Presumably for clang
hongchan
2017/05/11 21:13:02
This is a hack for clang-format module. The result
| |
| 42 {regexp: /test\-code\"\>(\n\s*\{){3}/, replace: 'test-code">'}, | |
|
Raymond Toy
2017/05/10 21:28:44
Why is the replacement "test-code>"?
hongchan
2017/05/11 21:13:01
Fixed with more specific patterns.
hongchan
2017/05/11 21:13:02
Basically |test-code>| is the beginning of the tes
| |
| 43 // Remove extra braces that are added for indented JS code. | |
| 44 {regexp: /(\}\n\s*){3}\<\/script\>/, replace: '</script>'}, | |
| 45 // Remove all the empty lines in html. | |
| 46 {regexp: /\>\n{2,}/, replace: '>\n'} | |
| 47 ] | |
| 48 }; | |
| 49 | |
| 50 | |
| 51 const Bar80 = { | |
|
Raymond Toy
2017/05/10 21:28:44
What is this for?
hongchan
2017/05/11 21:13:01
This prints out the 80 col ruler top and bottom, o
| |
| 52 Start: '+.........' + '.'.repeat(60) + '.80.COLS.+', | |
| 53 End: '+.........' + '.'.repeat(60) + '.....EOF.+' | |
| 54 }; | |
| 55 | |
| 56 | |
| 57 /** | |
| 58 * Basic utilities. | |
| 59 */ | |
| 60 const Util = { | |
| 61 | |
| 62 logAndExit: (moduleName, messageString) => { | |
| 63 console.error('[layout-test-tidy::' + moduleName + '] ' + messageString); | |
| 64 process.exit(1); | |
| 65 }, | |
| 66 | |
| 67 loadFileToStringSync: (filePath) => { | |
| 68 return fs.readFileSync(filePath); | |
| 69 }, | |
| 70 | |
| 71 writeStringToFileSync: (pageString, filePath) => { | |
| 72 fs.writeFileSync(filePath, pageString); | |
| 73 } | |
| 74 | |
| 75 }; | |
| 76 | |
| 77 | |
| 78 /** | |
| 79 * Wrapper for external modules like HTMLTidy and clang format. | |
| 80 * @type {Object} | |
| 81 */ | |
| 82 const Module = { | |
| 83 | |
| 84 /** | |
| 85 * Perform a batch RegExp string substution. | |
| 86 * @param {String} targetString Target string. | |
| 87 * @param {Array} swapCollection Array of key-value pairs. {regexp: replace} | |
|
Raymond Toy
2017/05/10 21:28:44
"{regexp: replace}" looks weird for what you're tr
hongchan
2017/05/11 21:13:02
Done.
| |
| 88 * @return {String} | |
| 89 */ | |
| 90 runRegExpSwapSync: (targetString, regExpSwapCollection) => { | |
| 91 let tempString = targetString; | |
| 92 regExpSwapCollection.forEach((item) => { | |
| 93 let re = new RegExp(item.regexp, 'g'); | |
| 94 tempString = tempString.replace(re, item.replace); | |
| 95 }); | |
| 96 | |
| 97 return tempString; | |
| 98 }, | |
| 99 | |
| 100 /** | |
| 101 * Run HTMLTidy on input string with options. | |
| 102 * @param {String} pageString [description] | |
| 103 * @param {Object} options HTMLTidy option as key-value pair. | |
| 104 * @param {Task} task Associated Task object. | |
| 105 * @return {String} | |
| 106 */ | |
| 107 runHTMLTidySync: (pageString, options, task) => { | |
| 108 let tidyDoc = new libtidy.TidyDoc(); | |
| 109 for (let option in options) | |
| 110 tidyDoc.optSet(option, options[option]); | |
| 111 | |
| 112 // This actually process the data inside of |tidyDoc|. | |
| 113 let logs = ''; | |
| 114 logs += tidyDoc.parseBufferSync(Buffer(pageString)); | |
| 115 logs += tidyDoc.cleanAndRepairSync(); | |
| 116 logs += tidyDoc.runDiagnosticsSync(); | |
| 117 | |
| 118 task.addLog('Module.runHTMLTidySync', logs.split('\n')); | |
| 119 | |
| 120 return tidyDoc.saveBufferSync().toString(); | |
| 121 }, | |
| 122 | |
| 123 /** | |
| 124 * Run clang-format and return a promise. | |
| 125 * @param {String} codeString JS code to apply clang-format. | |
| 126 * @param {Array} clangFormatOption optios array for clang-format. | |
| 127 * @param {Task} task Associated Task object. | |
| 128 * @return {Promise} Processed code as string. | |
| 129 * @resolve {String} clang-formatted JS code as string. | |
| 130 * @reject {Error} | |
| 131 */ | |
| 132 runClangFormat: (codeString, clangFormatOption, task) => { | |
| 133 let clangFormatBinary = __dirname + '/node_modules/clang-format/bin/'; | |
|
Raymond Toy
2017/05/10 21:28:44
Does this mean we actually install our own copy of
hongchan
2017/05/11 21:13:02
Until the weird depot_tools issue is resolved, I p
| |
| 134 clangFormatBinary += (os.platform() === 'win32') | |
| 135 ? 'win32/clang-format.exe' | |
| 136 : os.platform() + "_" + os.arch() + '/clang-format'; | |
| 137 | |
| 138 return new Promise((resolve, reject) => { | |
| 139 let echo = cp.spawn('echo', [codeString]); | |
|
Raymond Toy
2017/05/10 21:28:44
IIUC, this basically runs a shell with the echo co
hongchan
2017/05/11 21:13:02
Well, that I don't understand and this worked perf
| |
| 140 let result = ''; | |
| 141 | |
| 142 // Be sure to pipe the result, not to stdio. | |
| 143 let clangFormat = cp.spawn(clangFormatBinary, | |
| 144 clangFormatOption, | |
| 145 {stdio: ['pipe', 'pipe', process.stderr]}); | |
| 146 | |
| 147 echo.stdout.pipe(clangFormat.stdin); | |
| 148 | |
| 149 // For debug purpose: | |
| 150 // clangFormat.stdout.pipe(process.stdout); | |
| 151 | |
| 152 clangFormat.stdout.on('data', (data) => { | |
| 153 // Capture the data streamed. | |
| 154 result += data; | |
| 155 }); | |
| 156 | |
| 157 clangFormat.stdout.on('close', (exitCode) => { | |
| 158 if (exitCode) { | |
| 159 Util.logAndExit('Module.runClangFormat', 'exit code = 1'); | |
| 160 } else { | |
| 161 task.addLog('Module.runClangFormat', 'clang-format was successful.'); | |
| 162 resolve(result); | |
| 163 } | |
| 164 }); | |
| 165 }); | |
| 166 }, | |
| 167 | |
| 168 /** | |
| 169 * Detect line overflow and record the line number to the task log. | |
| 170 * @param {String} pageOrCodeString HTML page or JS code data in string. | |
| 171 * @param {TidyTask} task Associated TidyTask object. | |
| 172 */ | |
| 173 detectLineOverflow: (pageOrCodeString, task) => { | |
| 174 let currentLineNumber = 0; | |
| 175 let index0 = 0; | |
| 176 let index1 = 0; | |
| 177 while (index0 < pageOrCodeString.length - 1) { | |
| 178 index1 = pageOrCodeString.indexOf('\n', index0); | |
| 179 if (index1 - index0 > 80) { | |
| 180 task.addLog('Module.detectLineOverflow', | |
| 181 'Overflow (> 80 cols.) at line ' + currentLineNumber + '.'); | |
| 182 } | |
| 183 currentLineNumber++; | |
| 184 index0 = index1 + 1; | |
| 185 } | |
| 186 } | |
| 187 | |
| 188 }; | |
| 189 | |
| 190 | |
| 191 /** | |
| 192 * DOM utilities. Process DOM processing after parsing the string by JSDOM. | |
| 193 */ | |
| 194 const DOMUtil = { | |
| 195 | |
| 196 /** | |
| 197 * Parse string, generate JSDOM object and return |document| element. | |
| 198 * @param {String} pageString An HTML page in string. | |
| 199 * @return {Document} A |document| object. | |
| 200 */ | |
| 201 getJSDOMFromStringSync: (pageString) => { | |
| 202 return new JSDOM(`${pageString}`); | |
| 203 // return jsdom_.window.document; | |
| 204 }, | |
| 205 | |
| 206 /** | |
| 207 * In-place tidy up head element. | |
| 208 * @param {Document} document A |document| object. | |
| 209 * @param {Task} task An associated Task object. | |
| 210 * @return {Void} | |
| 211 */ | |
| 212 tidyHeadElementSync: (document, task) => { | |
| 213 try { | |
| 214 // If the title is missing, add one with the file name. | |
|
Raymond Toy
2017/05/10 21:28:43
"with the" -> "from the"
hongchan
2017/05/11 21:13:01
Done.
| |
| 215 let titleElement = document.querySelector('title'); | |
| 216 if (!titleElement) { | |
| 217 titleElement = document.createElement('title'); | |
| 218 titleElement.textContent = path.basename(task.targetFilePath_); | |
| 219 task.addLog('DOMUtil.tidyHeadElementSync', | |
| 220 'Title element was missing thus a new one was added.'); | |
| 221 } | |
| 222 | |
| 223 // The title element should be the first. | |
| 224 let headElement = document.querySelector('head'); | |
| 225 headElement.insertBefore(titleElement, headElement.firstChild); | |
| 226 | |
| 227 // If a script element in body does not have JS code, move to the head | |
| 228 // section. | |
|
Raymond Toy
2017/05/10 21:28:44
Do we really want to do this? Can't think of anyth
hongchan
2017/05/11 21:13:01
Yes, we do want to have this.
All the scripts in
| |
| 229 let scriptElementsInBody = document.body.querySelectorAll('script'); | |
| 230 scriptElementsInBody.forEach((scriptElement) => { | |
| 231 if (!scriptElement.textContent) | |
| 232 headElement.appendChild(scriptElement); | |
| 233 }); | |
| 234 } catch (error) { | |
| 235 task.addLog('DOMUtil.tidyHeadElementSync', error.toString()); | |
| 236 } | |
| 237 }, | |
| 238 | |
| 239 /** | |
| 240 * Sanitize and extract |script| element with JS test code. | |
| 241 * @param {Document} document A |document| object. | |
| 242 * @param {Task} task An associated Task object. | |
| 243 * @return {ScriptElement} | |
| 244 */ | |
| 245 getElementWithTestCodeSync: (document, task) => { | |
| 246 let numberOfElementsWithCode = 0; | |
| 247 let elementWithTestCode; | |
| 248 let scriptElements = document.querySelectorAll('script'); | |
| 249 | |
| 250 scriptElements.forEach(function (element) { | |
| 251 // We don't want type attribute. | |
| 252 element.removeAttribute('type'); | |
| 253 | |
| 254 if (element.textContent.length > 0) { | |
| 255 ++numberOfElementsWithCode; | |
| 256 elementWithTestCode = element; | |
| 257 element.id = 'layout-test-code'; | |
|
Raymond Toy
2017/05/10 21:28:44
Is this safe? What if another element has the sam
hongchan
2017/05/11 21:13:01
If there are two or more script elements with the
| |
| 258 // If the element was belong to something else than body, move it. | |
|
Raymond Toy
2017/05/10 21:28:44
"was belong" -> "belongs", I think.
"else than" ->
hongchan
2017/05/11 21:13:02
Done.
| |
| 259 if (element.parentElement !== document.body) | |
| 260 document.body.appendChild(element); | |
| 261 } | |
| 262 }); | |
| 263 | |
| 264 if (numberOfElementsWithCode !== 1) { | |
| 265 task.addLog('DOMUtil.getElementWithTestCodeSync', | |
|
Raymond Toy
2017/05/10 21:28:43
Is it really bad to have more than one? What if we
hongchan
2017/05/11 21:13:01
Yes, it's bad. If there are two script tags with a
| |
| 266 numberOfElementsWithCode + ' <script> element(s) with JS ' + | |
| 267 'code were found.'); | |
| 268 elementWithTestCode = null; | |
| 269 } | |
| 270 | |
| 271 return elementWithTestCode; | |
| 272 } | |
| 273 | |
| 274 }; | |
| 275 | |
| 276 | |
| 277 /** | |
| 278 * @class TidyTask | |
| 279 * @description Per-file processing task. This object should be constructed | |
| 280 * directly. The task runner creates this when it is necessary. | |
| 281 */ | |
| 282 class TidyTask { | |
| 283 | |
| 284 /** | |
| 285 * @param {String} targetFilePath A path to file to be processed. | |
| 286 * @param {Boolean} isDryRun Print out the result to |stdout| when true. By | |
| 287 * default, this is true. | |
| 288 */ | |
| 289 constructor (targetFilePath, isDryRun) { | |
| 290 this.targetFilePath_ = targetFilePath; | |
| 291 this.isDryRun_ = isDryRun; | |
| 292 | |
| 293 this.fileType_ = path.extname(this.targetFilePath_); | |
| 294 this.pageString_ = Util.loadFileToStringSync(this.targetFilePath_); | |
| 295 this.jsdom_ = null; | |
| 296 this.logs_ = {}; | |
| 297 } | |
| 298 | |
| 299 /** | |
| 300 * Run processing sequence. Don't call this directly. | |
| 301 * @param {Function} taskDone Task runner callback function. | |
| 302 */ | |
| 303 run (taskDone) { | |
| 304 switch (this.fileType_) { | |
| 305 case '.html': | |
| 306 this.processHTML_(taskDone); | |
| 307 break; | |
| 308 case '.js': | |
| 309 this.processJS_(taskDone); | |
| 310 break; | |
| 311 default: | |
| 312 Util.logAndExit('TidyTask.constructor', 'Invalid file type: ' | |
| 313 + this.fileType_); | |
| 314 break; | |
| 315 } | |
| 316 } | |
| 317 | |
| 318 processHTML_ (taskDone) { | |
| 319 // Parse page string into JSDOM.element object. | |
| 320 this.jsdom_ = DOMUtil.getJSDOMFromStringSync(this.pageString_); | |
| 321 | |
| 322 // Clean up the head element section. | |
| 323 DOMUtil.tidyHeadElementSync(this.jsdom_.window.document, this); | |
| 324 | |
| 325 let scriptElement = | |
| 326 DOMUtil.getElementWithTestCodeSync(this.jsdom_.window.document, this); | |
| 327 | |
| 328 if (!scriptElement) | |
| 329 Util.logAndExit('TidyTask.processHTML_', 'Invalid <script> element.'); | |
| 330 | |
| 331 // Extract JS code string with additional braces for indentation hack. | |
|
Raymond Toy
2017/05/10 21:28:44
Add note that the number of braces depends on the
hongchan
2017/05/11 21:13:01
Currently the main script is always two level lowe
| |
| 332 let codeString = '{\n{\n{' + scriptElement.textContent + '}\n}\n}'; | |
|
Raymond Toy
2017/05/10 21:28:44
When I did this by hand, I inserted "{{{\n" and ap
| |
| 333 | |
| 334 // Start with clang-foramt and text-based operation. | |
|
Raymond Toy
2017/05/10 21:28:43
What does "Start" mean here? What will you end wi
hongchan
2017/05/11 21:13:02
Done.
| |
| 335 Module.runClangFormat(codeString, OPTIONS.ClangFormat, this) | |
| 336 .then((formattedCodeString) => { | |
| 337 // Replace the original code with clang-formatted code. | |
| 338 scriptElement.textContent = formattedCodeString; | |
| 339 | |
| 340 // Then tidy the text data from JSDOM. After this point, DOM | |
| 341 // manipulation is not possible anymore. | |
| 342 let pageString = this.jsdom_.serialize(); | |
| 343 pageString = Module.runHTMLTidySync( | |
| 344 pageString, OPTIONS.HTMLTidy, this); | |
| 345 pageString = Module.runRegExpSwapSync( | |
| 346 pageString, OPTIONS.RegExpSwapCollection); | |
| 347 | |
| 348 Module.detectLineOverflow(pageString, this); | |
| 349 | |
| 350 this.finish_(pageString, taskDone); | |
| 351 }); | |
| 352 } | |
| 353 | |
| 354 processJS_ (taskDone) { | |
| 355 // The file is a JS code, so run clang-format directly. | |
| 356 Module.runClangFormat(this.pageString_, OPTIONS.ClangFormat, this) | |
| 357 .then((formattedCodeString) => { | |
| 358 formattedCodeString = Module.runRegExpSwapSync( | |
| 359 formattedCodeString, [OPTIONS.RegExpSwapCollection[0]]); | |
| 360 | |
| 361 Module.detectLineOverflow(formattedCodeString, this); | |
| 362 | |
| 363 this.finish_(formattedCodeString, taskDone); | |
| 364 }); | |
| 365 } | |
| 366 | |
| 367 finish_ (resultString, taskDone) { | |
| 368 // Print out the result to console when 'Dry Run', otherwise save | |
| 369 // directly to the target file path. | |
| 370 if (this.isDryRun_) { | |
| 371 console.log(Bar80.Start + '\n' + resultString + '\n' + Bar80.End); | |
| 372 } else { | |
| 373 Util.writeStringToFileSync(resultString, this.targetFilePath_); | |
| 374 } | |
| 375 | |
| 376 this.printLog(); | |
| 377 taskDone(); | |
| 378 } | |
| 379 | |
| 380 /** | |
| 381 * Adding log message. | |
| 382 * @param {String} location Caller information. | |
| 383 * @param {String} message Log message. | |
| 384 */ | |
| 385 addLog (location, message) { | |
| 386 if (!this.logs_.hasOwnProperty(location)) | |
| 387 this.logs_[location] = []; | |
| 388 this.logs_[location].push(message); | |
| 389 } | |
| 390 | |
| 391 /** | |
| 392 * Print log messages at the end of task. | |
| 393 */ | |
| 394 printLog () { | |
| 395 console.log('> Logs from: ' + this.targetFilePath_); | |
| 396 for (let location in this.logs_) { | |
| 397 console.log(' [] ' + location); | |
| 398 this.logs_[location].forEach((message) => { | |
| 399 if (Array.isArray(message)) { | |
| 400 message.forEach((subMessage) => { | |
| 401 if (subMessage.length > 0) | |
| 402 console.log(' - ' + subMessage); | |
| 403 }); | |
| 404 } else { | |
| 405 console.log(' - ' + message); | |
| 406 } | |
| 407 }); | |
| 408 } | |
| 409 } | |
| 410 | |
| 411 } | |
| 412 | |
| 413 | |
| 414 /** | |
| 415 * @class TidyTaskRunner | |
| 416 */ | |
| 417 class TidyTaskRunner { | |
| 418 | |
| 419 /** | |
| 420 * @param {Array} files A list of file paths. | |
| 421 * @param {Boolean} isDryRun Dry run flag. When |true| the processed output | |
| 422 * will be printed out via stdout instead of actual | |
| 423 * target files. | |
| 424 * @return {TidyTaskRunner} A task runner object. | |
| 425 */ | |
| 426 constructor (files, isDryRun) { | |
| 427 this.targetFiles_ = files; | |
| 428 this.isDryRun_ = isDryRun; | |
| 429 | |
| 430 this.tasks_ = []; | |
| 431 this.currentTask_ = 0; | |
| 432 this.oncomplete = null; | |
| 433 } | |
| 434 | |
| 435 startProcessing () { | |
| 436 this.targetFiles_.forEach((filePath) => { | |
| 437 this.tasks_.push(new TidyTask(filePath, this.isDryRun_)); | |
| 438 }); | |
| 439 | |
| 440 console.log('[layout-test-tidy] Task runner started: ' | |
| 441 + this.targetFiles_.length + ' file(s).'); | |
| 442 this.runTask_(); | |
| 443 } | |
| 444 | |
| 445 runTask_ () { | |
| 446 console.log('[layout-test-tidy] Running task #' + (this.currentTask_ + 1) | |
| 447 + ': ' + this.targetFiles_[this.currentTask_] | |
| 448 + (this.isDryRun_ ? ' (DRYRUN)' : '')); | |
| 449 this.tasks_[this.currentTask_].run(this.done_.bind(this)); | |
| 450 } | |
| 451 | |
| 452 done_ () { | |
| 453 console.log('[layout-test-tidy] Task #' + (this.currentTask_ + 1) | |
| 454 + ' completed.\n'); | |
| 455 this.currentTask_++; | |
| 456 if (this.currentTask_ < this.tasks_.length) { | |
| 457 this.runTask_(); | |
| 458 } else { | |
| 459 console.log('[layout-test-tidy] Task runner completed: ' | |
| 460 + this.targetFiles_.length + ' file(s) processed.'); | |
| 461 if (this.oncomplete) | |
| 462 this.oncomplete(); | |
| 463 } | |
| 464 } | |
| 465 | |
| 466 } | |
| 467 | |
| 468 | |
| 469 // Entry point. | |
| 470 function main() { | |
| 471 let args = process.argv.slice(2); | |
| 472 if (!args || args.length == 0 || args.length > 2) | |
| 473 Util.logAndExit('main', 'Invalid arguments. (' + args + ')'); | |
| 474 | |
| 475 let targetPath = args[0]; | |
| 476 let isDryRun = args[1] === '--dryrun'; | |
| 477 | |
| 478 let files = []; | |
| 479 | |
| 480 if (targetPath) { | |
| 481 try { | |
| 482 let stat = fs.lstatSync(targetPath); | |
| 483 if (stat.isFile()) { | |
| 484 files.push(targetPath); | |
| 485 } else if (stat.isDirectory()) { | |
| 486 files = glob.sync(targetPath + '/**/*.{html,js}'); | |
| 487 } | |
| 488 } catch (error) { | |
| 489 let errorMessage = 'Invalid file path. (' + targetPath + ')\n' | |
| 490 + ' > ' + error.toString(); | |
| 491 Util.logAndExit('main', errorMessage); | |
| 492 } | |
| 493 } | |
| 494 | |
| 495 // Files to be skipped. | |
| 496 let filesToBeSkipped = | |
| 497 Util.loadFileToStringSync('skip-tidy').toString().split('\n'); | |
| 498 filesToBeSkipped.forEach((fileSkipped) => { | |
| 499 let index = files.indexOf(fileSkipped); | |
| 500 if (index > -1) { | |
| 501 files.splice(index, 1); | |
| 502 } | |
| 503 }); | |
| 504 | |
| 505 if (files.length > 0) { | |
| 506 let taskRunner = new TidyTaskRunner(files, isDryRun); | |
| 507 taskRunner.startProcessing(); | |
| 508 } else { | |
| 509 Util.logAndExit('main', 'No files to process.'); | |
| 510 } | |
| 511 } | |
| 512 | |
| 513 main(); | |
| OLD | NEW |