CompileManager.js 24 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759
  1. /* eslint-disable
  2. camelcase,
  3. handle-callback-err,
  4. no-return-assign,
  5. no-undef,
  6. no-unused-vars,
  7. */
  8. // TODO: This file was created by bulk-decaffeinate.
  9. // Fix any style issues and re-enable lint.
  10. /*
  11. * decaffeinate suggestions:
  12. * DS101: Remove unnecessary use of Array.from
  13. * DS102: Remove unnecessary code created because of implicit returns
  14. * DS103: Rewrite code to no longer use __guard__
  15. * DS207: Consider shorter variations of null checks
  16. * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
  17. */
  18. let CompileManager
  19. const ResourceWriter = require('./ResourceWriter')
  20. const LatexRunner = require('./LatexRunner')
  21. const OutputFileFinder = require('./OutputFileFinder')
  22. const OutputCacheManager = require('./OutputCacheManager')
  23. const Settings = require('@overleaf/settings')
  24. const Path = require('path')
  25. const logger = require('logger-sharelatex')
  26. const Metrics = require('./Metrics')
  27. const child_process = require('child_process')
  28. const DraftModeManager = require('./DraftModeManager')
  29. const TikzManager = require('./TikzManager')
  30. const LockManager = require('./LockManager')
  31. const fs = require('fs')
  32. const fse = require('fs-extra')
  33. const os = require('os')
  34. const async = require('async')
  35. const Errors = require('./Errors')
  36. const CommandRunner = require('./CommandRunner')
  37. const { emitPdfStats } = require('./ContentCacheMetrics')
  38. const getCompileName = function (project_id, user_id) {
  39. if (user_id != null) {
  40. return `${project_id}-${user_id}`
  41. } else {
  42. return project_id
  43. }
  44. }
  45. const getCompileDir = (project_id, user_id) =>
  46. Path.join(Settings.path.compilesDir, getCompileName(project_id, user_id))
  47. const getOutputDir = (project_id, user_id) =>
  48. Path.join(Settings.path.outputDir, getCompileName(project_id, user_id))
  49. module.exports = CompileManager = {
  50. doCompileWithLock(request, callback) {
  51. if (callback == null) {
  52. callback = function (error, outputFiles) {}
  53. }
  54. const compileDir = getCompileDir(request.project_id, request.user_id)
  55. const lockFile = Path.join(compileDir, '.project-lock')
  56. // use a .project-lock file in the compile directory to prevent
  57. // simultaneous compiles
  58. return fse.ensureDir(compileDir, function (error) {
  59. if (error != null) {
  60. return callback(error)
  61. }
  62. return LockManager.runWithLock(
  63. lockFile,
  64. (releaseLock) => CompileManager.doCompile(request, releaseLock),
  65. callback
  66. )
  67. })
  68. },
  69. doCompile(request, callback) {
  70. if (callback == null) {
  71. callback = function (error, outputFiles) {}
  72. }
  73. const compileDir = getCompileDir(request.project_id, request.user_id)
  74. const outputDir = getOutputDir(request.project_id, request.user_id)
  75. const timerE2E = new Metrics.Timer('compile-e2e')
  76. let timer = new Metrics.Timer('write-to-disk')
  77. logger.log(
  78. { project_id: request.project_id, user_id: request.user_id },
  79. 'syncing resources to disk'
  80. )
  81. return ResourceWriter.syncResourcesToDisk(request, compileDir, function (
  82. error,
  83. resourceList
  84. ) {
  85. // NOTE: resourceList is insecure, it should only be used to exclude files from the output list
  86. if (error != null && error instanceof Errors.FilesOutOfSyncError) {
  87. logger.warn(
  88. { project_id: request.project_id, user_id: request.user_id },
  89. 'files out of sync, please retry'
  90. )
  91. return callback(error)
  92. } else if (error != null) {
  93. logger.err(
  94. {
  95. err: error,
  96. project_id: request.project_id,
  97. user_id: request.user_id
  98. },
  99. 'error writing resources to disk'
  100. )
  101. return callback(error)
  102. }
  103. logger.log(
  104. {
  105. project_id: request.project_id,
  106. user_id: request.user_id,
  107. time_taken: Date.now() - timer.start
  108. },
  109. 'written files to disk'
  110. )
  111. const syncStage = timer.done()
  112. const injectDraftModeIfRequired = function (callback) {
  113. if (request.draft) {
  114. return DraftModeManager.injectDraftMode(
  115. Path.join(compileDir, request.rootResourcePath),
  116. callback
  117. )
  118. } else {
  119. return callback()
  120. }
  121. }
  122. const createTikzFileIfRequired = (callback) =>
  123. TikzManager.checkMainFile(
  124. compileDir,
  125. request.rootResourcePath,
  126. resourceList,
  127. function (error, needsMainFile) {
  128. if (error != null) {
  129. return callback(error)
  130. }
  131. if (needsMainFile) {
  132. return TikzManager.injectOutputFile(
  133. compileDir,
  134. request.rootResourcePath,
  135. callback
  136. )
  137. } else {
  138. return callback()
  139. }
  140. }
  141. )
  142. // set up environment variables for chktex
  143. const env = {}
  144. if (Settings.texliveOpenoutAny && Settings.texliveOpenoutAny !== '') {
  145. // override default texlive openout_any environment variable
  146. env.openout_any = Settings.texliveOpenoutAny
  147. }
  148. // only run chktex on LaTeX files (not knitr .Rtex files or any others)
  149. const isLaTeXFile =
  150. request.rootResourcePath != null
  151. ? request.rootResourcePath.match(/\.tex$/i)
  152. : undefined
  153. if (request.check != null && isLaTeXFile) {
  154. env.CHKTEX_OPTIONS = '-nall -e9 -e10 -w15 -w16'
  155. env.CHKTEX_ULIMIT_OPTIONS = '-t 5 -v 64000'
  156. if (request.check === 'error') {
  157. env.CHKTEX_EXIT_ON_ERROR = 1
  158. }
  159. if (request.check === 'validate') {
  160. env.CHKTEX_VALIDATE = 1
  161. }
  162. }
  163. // apply a series of file modifications/creations for draft mode and tikz
  164. return async.series(
  165. [injectDraftModeIfRequired, createTikzFileIfRequired],
  166. function (error) {
  167. if (error != null) {
  168. return callback(error)
  169. }
  170. timer = new Metrics.Timer('run-compile')
  171. // find the image tag to log it as a metric, e.g. 2015.1 (convert . to - for graphite)
  172. let tag =
  173. __guard__(
  174. __guard__(
  175. request.imageName != null
  176. ? request.imageName.match(/:(.*)/)
  177. : undefined,
  178. (x1) => x1[1]
  179. ),
  180. (x) => x.replace(/\./g, '-')
  181. ) || 'default'
  182. if (!request.project_id.match(/^[0-9a-f]{24}$/)) {
  183. tag = 'other'
  184. } // exclude smoke test
  185. Metrics.inc('compiles')
  186. Metrics.inc(`compiles-with-image.${tag}`)
  187. const compileName = getCompileName(
  188. request.project_id,
  189. request.user_id
  190. )
  191. return LatexRunner.runLatex(
  192. compileName,
  193. {
  194. directory: compileDir,
  195. mainFile: request.rootResourcePath,
  196. compiler: request.compiler,
  197. timeout: request.timeout,
  198. image: request.imageName,
  199. flags: request.flags,
  200. environment: env,
  201. compileGroup: request.compileGroup
  202. },
  203. function (error, output, stats, timings) {
  204. // request was for validation only
  205. let metric_key, metric_value
  206. if (request.check === 'validate') {
  207. const result = (error != null ? error.code : undefined)
  208. ? 'fail'
  209. : 'pass'
  210. error = new Error('validation')
  211. error.validate = result
  212. }
  213. // request was for compile, and failed on validation
  214. if (
  215. request.check === 'error' &&
  216. (error != null ? error.message : undefined) === 'exited'
  217. ) {
  218. error = new Error('compilation')
  219. error.validate = 'fail'
  220. }
  221. // compile was killed by user, was a validation, or a compile which failed validation
  222. if (
  223. (error != null ? error.terminated : undefined) ||
  224. (error != null ? error.validate : undefined) ||
  225. (error != null ? error.timedout : undefined)
  226. ) {
  227. OutputFileFinder.findOutputFiles(
  228. resourceList,
  229. compileDir,
  230. function (err, outputFiles) {
  231. if (err != null) {
  232. return callback(err)
  233. }
  234. error.outputFiles = outputFiles // return output files so user can check logs
  235. return callback(error)
  236. }
  237. )
  238. return
  239. }
  240. // compile completed normally
  241. if (error != null) {
  242. return callback(error)
  243. }
  244. Metrics.inc('compiles-succeeded')
  245. stats = stats || {}
  246. const object = stats || {}
  247. for (metric_key in object) {
  248. metric_value = object[metric_key]
  249. Metrics.count(metric_key, metric_value)
  250. }
  251. timings = timings || {}
  252. const object1 = timings || {}
  253. for (metric_key in object1) {
  254. metric_value = object1[metric_key]
  255. Metrics.timing(metric_key, metric_value)
  256. }
  257. const loadavg =
  258. typeof os.loadavg === 'function' ? os.loadavg() : undefined
  259. if (loadavg != null) {
  260. Metrics.gauge('load-avg', loadavg[0])
  261. }
  262. const ts = timer.done()
  263. logger.log(
  264. {
  265. project_id: request.project_id,
  266. user_id: request.user_id,
  267. time_taken: ts,
  268. stats,
  269. timings,
  270. loadavg
  271. },
  272. 'done compile'
  273. )
  274. if ((stats != null ? stats['latex-runs'] : undefined) > 0) {
  275. Metrics.timing('run-compile-per-pass', ts / stats['latex-runs'])
  276. }
  277. if (
  278. (stats != null ? stats['latex-runs'] : undefined) > 0 &&
  279. (timings != null ? timings['cpu-time'] : undefined) > 0
  280. ) {
  281. Metrics.timing(
  282. 'run-compile-cpu-time-per-pass',
  283. timings['cpu-time'] / stats['latex-runs']
  284. )
  285. }
  286. // Emit compile time.
  287. timings.compile = ts
  288. timer = new Metrics.Timer('process-output-files')
  289. return OutputFileFinder.findOutputFiles(
  290. resourceList,
  291. compileDir,
  292. function (error, outputFiles) {
  293. if (error != null) {
  294. return callback(error)
  295. }
  296. return OutputCacheManager.saveOutputFiles(
  297. { request, stats, timings },
  298. outputFiles,
  299. compileDir,
  300. outputDir,
  301. (err, newOutputFiles) => {
  302. if (err) {
  303. const {
  304. project_id: projectId,
  305. user_id: userId
  306. } = request
  307. logger.err(
  308. { projectId, userId, err },
  309. 'failed to save output files'
  310. )
  311. }
  312. const outputStage = timer.done()
  313. timings.sync = syncStage
  314. timings.output = outputStage
  315. // Emit e2e compile time.
  316. timings.compileE2E = timerE2E.done()
  317. if (stats['pdf-size']) {
  318. emitPdfStats(stats, timings)
  319. }
  320. callback(null, newOutputFiles, stats, timings)
  321. }
  322. )
  323. }
  324. )
  325. }
  326. )
  327. }
  328. )
  329. })
  330. },
  331. stopCompile(project_id, user_id, callback) {
  332. if (callback == null) {
  333. callback = function (error) {}
  334. }
  335. const compileName = getCompileName(project_id, user_id)
  336. return LatexRunner.killLatex(compileName, callback)
  337. },
  338. clearProject(project_id, user_id, _callback) {
  339. if (_callback == null) {
  340. _callback = function (error) {}
  341. }
  342. const callback = function (error) {
  343. _callback(error)
  344. return (_callback = function () {})
  345. }
  346. const compileDir = getCompileDir(project_id, user_id)
  347. const outputDir = getOutputDir(project_id, user_id)
  348. return CompileManager._checkDirectory(compileDir, function (err, exists) {
  349. if (err != null) {
  350. return callback(err)
  351. }
  352. if (!exists) {
  353. return callback()
  354. } // skip removal if no directory present
  355. const proc = child_process.spawn('rm', [
  356. '-r',
  357. '-f',
  358. '--',
  359. compileDir,
  360. outputDir
  361. ])
  362. proc.on('error', callback)
  363. let stderr = ''
  364. proc.stderr.setEncoding('utf8').on('data', (chunk) => (stderr += chunk))
  365. return proc.on('close', function (code) {
  366. if (code === 0) {
  367. return callback(null)
  368. } else {
  369. return callback(
  370. new Error(`rm -r ${compileDir} ${outputDir} failed: ${stderr}`)
  371. )
  372. }
  373. })
  374. })
  375. },
  376. _findAllDirs(callback) {
  377. if (callback == null) {
  378. callback = function (error, allDirs) {}
  379. }
  380. const root = Settings.path.compilesDir
  381. return fs.readdir(root, function (err, files) {
  382. if (err != null) {
  383. return callback(err)
  384. }
  385. const allDirs = Array.from(files).map((file) => Path.join(root, file))
  386. return callback(null, allDirs)
  387. })
  388. },
  389. clearExpiredProjects(max_cache_age_ms, callback) {
  390. if (callback == null) {
  391. callback = function (error) {}
  392. }
  393. const now = Date.now()
  394. // action for each directory
  395. const expireIfNeeded = (checkDir, cb) =>
  396. fs.stat(checkDir, function (err, stats) {
  397. if (err != null) {
  398. return cb()
  399. } // ignore errors checking directory
  400. const age = now - stats.mtime
  401. const hasExpired = age > max_cache_age_ms
  402. if (hasExpired) {
  403. return fse.remove(checkDir, cb)
  404. } else {
  405. return cb()
  406. }
  407. })
  408. // iterate over all project directories
  409. return CompileManager._findAllDirs(function (error, allDirs) {
  410. if (error != null) {
  411. return callback()
  412. }
  413. return async.eachSeries(allDirs, expireIfNeeded, callback)
  414. })
  415. },
  416. _checkDirectory(compileDir, callback) {
  417. if (callback == null) {
  418. callback = function (error, exists) {}
  419. }
  420. return fs.lstat(compileDir, function (err, stats) {
  421. if ((err != null ? err.code : undefined) === 'ENOENT') {
  422. return callback(null, false) // directory does not exist
  423. } else if (err != null) {
  424. logger.err(
  425. { dir: compileDir, err },
  426. 'error on stat of project directory for removal'
  427. )
  428. return callback(err)
  429. } else if (!(stats != null ? stats.isDirectory() : undefined)) {
  430. logger.err(
  431. { dir: compileDir, stats },
  432. 'bad project directory for removal'
  433. )
  434. return callback(new Error('project directory is not directory'))
  435. } else {
  436. return callback(null, true)
  437. }
  438. })
  439. }, // directory exists
  440. syncFromCode(
  441. project_id,
  442. user_id,
  443. file_name,
  444. line,
  445. column,
  446. imageName,
  447. callback
  448. ) {
  449. // If LaTeX was run in a virtual environment, the file path that synctex expects
  450. // might not match the file path on the host. The .synctex.gz file however, will be accessed
  451. // wherever it is on the host.
  452. if (callback == null) {
  453. callback = function (error, pdfPositions) {}
  454. }
  455. const compileName = getCompileName(project_id, user_id)
  456. const base_dir = Settings.path.synctexBaseDir(compileName)
  457. const file_path = base_dir + '/' + file_name
  458. const compileDir = getCompileDir(project_id, user_id)
  459. const synctex_path = `${base_dir}/output.pdf`
  460. const command = ['code', synctex_path, file_path, line, column]
  461. CompileManager._runSynctex(
  462. project_id,
  463. user_id,
  464. command,
  465. imageName,
  466. function (error, stdout) {
  467. if (error != null) {
  468. return callback(error)
  469. }
  470. logger.log(
  471. { project_id, user_id, file_name, line, column, command, stdout },
  472. 'synctex code output'
  473. )
  474. return callback(
  475. null,
  476. CompileManager._parseSynctexFromCodeOutput(stdout)
  477. )
  478. }
  479. )
  480. },
  481. syncFromPdf(project_id, user_id, page, h, v, imageName, callback) {
  482. if (callback == null) {
  483. callback = function (error, filePositions) {}
  484. }
  485. const compileName = getCompileName(project_id, user_id)
  486. const compileDir = getCompileDir(project_id, user_id)
  487. const base_dir = Settings.path.synctexBaseDir(compileName)
  488. const synctex_path = `${base_dir}/output.pdf`
  489. const command = ['pdf', synctex_path, page, h, v]
  490. CompileManager._runSynctex(
  491. project_id,
  492. user_id,
  493. command,
  494. imageName,
  495. function (error, stdout) {
  496. if (error != null) {
  497. return callback(error)
  498. }
  499. logger.log(
  500. { project_id, user_id, page, h, v, stdout },
  501. 'synctex pdf output'
  502. )
  503. return callback(
  504. null,
  505. CompileManager._parseSynctexFromPdfOutput(stdout, base_dir)
  506. )
  507. }
  508. )
  509. },
  510. _checkFileExists(dir, filename, callback) {
  511. if (callback == null) {
  512. callback = function (error) {}
  513. }
  514. const file = Path.join(dir, filename)
  515. return fs.stat(dir, function (error, stats) {
  516. if ((error != null ? error.code : undefined) === 'ENOENT') {
  517. return callback(new Errors.NotFoundError('no output directory'))
  518. }
  519. if (error != null) {
  520. return callback(error)
  521. }
  522. return fs.stat(file, function (error, stats) {
  523. if ((error != null ? error.code : undefined) === 'ENOENT') {
  524. return callback(new Errors.NotFoundError('no output file'))
  525. }
  526. if (error != null) {
  527. return callback(error)
  528. }
  529. if (!(stats != null ? stats.isFile() : undefined)) {
  530. return callback(new Error('not a file'))
  531. }
  532. return callback()
  533. })
  534. })
  535. },
  536. _runSynctex(project_id, user_id, command, imageName, callback) {
  537. if (callback == null) {
  538. callback = function (error, stdout) {}
  539. }
  540. const seconds = 1000
  541. command.unshift('/opt/synctex')
  542. const directory = getCompileDir(project_id, user_id)
  543. const timeout = 60 * 1000 // increased to allow for large projects
  544. const compileName = getCompileName(project_id, user_id)
  545. const compileGroup = 'synctex'
  546. CompileManager._checkFileExists(directory, 'output.synctex.gz', (error) => {
  547. if (error) {
  548. return callback(error)
  549. }
  550. return CommandRunner.run(
  551. compileName,
  552. command,
  553. directory,
  554. imageName ||
  555. (Settings.clsi && Settings.clsi.docker
  556. ? Settings.clsi.docker.image
  557. : undefined),
  558. timeout,
  559. {},
  560. compileGroup,
  561. function (error, output) {
  562. if (error != null) {
  563. logger.err(
  564. { err: error, command, project_id, user_id },
  565. 'error running synctex'
  566. )
  567. return callback(error)
  568. }
  569. return callback(null, output.stdout)
  570. }
  571. )
  572. })
  573. },
  574. _parseSynctexFromCodeOutput(output) {
  575. const results = []
  576. for (const line of Array.from(output.split('\n'))) {
  577. const [node, page, h, v, width, height] = Array.from(line.split('\t'))
  578. if (node === 'NODE') {
  579. results.push({
  580. page: parseInt(page, 10),
  581. h: parseFloat(h),
  582. v: parseFloat(v),
  583. height: parseFloat(height),
  584. width: parseFloat(width)
  585. })
  586. }
  587. }
  588. return results
  589. },
  590. _parseSynctexFromPdfOutput(output, base_dir) {
  591. const results = []
  592. for (let line of Array.from(output.split('\n'))) {
  593. let column, file_path, node
  594. ;[node, file_path, line, column] = Array.from(line.split('\t'))
  595. if (node === 'NODE') {
  596. const file = file_path.slice(base_dir.length + 1)
  597. results.push({
  598. file,
  599. line: parseInt(line, 10),
  600. column: parseInt(column, 10)
  601. })
  602. }
  603. }
  604. return results
  605. },
  606. wordcount(project_id, user_id, file_name, image, callback) {
  607. if (callback == null) {
  608. callback = function (error, pdfPositions) {}
  609. }
  610. logger.log({ project_id, user_id, file_name, image }, 'running wordcount')
  611. const file_path = `$COMPILE_DIR/${file_name}`
  612. const command = [
  613. 'texcount',
  614. '-nocol',
  615. '-inc',
  616. file_path,
  617. `-out=${file_path}.wc`
  618. ]
  619. const compileDir = getCompileDir(project_id, user_id)
  620. const timeout = 60 * 1000
  621. const compileName = getCompileName(project_id, user_id)
  622. const compileGroup = 'wordcount'
  623. return fse.ensureDir(compileDir, function (error) {
  624. if (error != null) {
  625. logger.err(
  626. { error, project_id, user_id, file_name },
  627. 'error ensuring dir for sync from code'
  628. )
  629. return callback(error)
  630. }
  631. return CommandRunner.run(
  632. compileName,
  633. command,
  634. compileDir,
  635. image,
  636. timeout,
  637. {},
  638. compileGroup,
  639. function (error) {
  640. if (error != null) {
  641. return callback(error)
  642. }
  643. return fs.readFile(
  644. compileDir + '/' + file_name + '.wc',
  645. 'utf-8',
  646. function (err, stdout) {
  647. if (err != null) {
  648. // call it node_err so sentry doesn't use random path error as unique id so it can't be ignored
  649. logger.err(
  650. { node_err: err, command, compileDir, project_id, user_id },
  651. 'error reading word count output'
  652. )
  653. return callback(err)
  654. }
  655. const results = CompileManager._parseWordcountFromOutput(stdout)
  656. logger.log(
  657. { project_id, user_id, wordcount: results },
  658. 'word count results'
  659. )
  660. return callback(null, results)
  661. }
  662. )
  663. }
  664. )
  665. })
  666. },
  667. _parseWordcountFromOutput(output) {
  668. const results = {
  669. encode: '',
  670. textWords: 0,
  671. headWords: 0,
  672. outside: 0,
  673. headers: 0,
  674. elements: 0,
  675. mathInline: 0,
  676. mathDisplay: 0,
  677. errors: 0,
  678. messages: ''
  679. }
  680. for (const line of Array.from(output.split('\n'))) {
  681. const [data, info] = Array.from(line.split(':'))
  682. if (data.indexOf('Encoding') > -1) {
  683. results.encode = info.trim()
  684. }
  685. if (data.indexOf('in text') > -1) {
  686. results.textWords = parseInt(info, 10)
  687. }
  688. if (data.indexOf('in head') > -1) {
  689. results.headWords = parseInt(info, 10)
  690. }
  691. if (data.indexOf('outside') > -1) {
  692. results.outside = parseInt(info, 10)
  693. }
  694. if (data.indexOf('of head') > -1) {
  695. results.headers = parseInt(info, 10)
  696. }
  697. if (data.indexOf('Number of floats/tables/figures') > -1) {
  698. results.elements = parseInt(info, 10)
  699. }
  700. if (data.indexOf('Number of math inlines') > -1) {
  701. results.mathInline = parseInt(info, 10)
  702. }
  703. if (data.indexOf('Number of math displayed') > -1) {
  704. results.mathDisplay = parseInt(info, 10)
  705. }
  706. if (data === '(errors') {
  707. // errors reported as (errors:123)
  708. results.errors = parseInt(info, 10)
  709. }
  710. if (line.indexOf('!!! ') > -1) {
  711. // errors logged as !!! message !!!
  712. results.messages += line + '\n'
  713. }
  714. }
  715. return results
  716. }
  717. }
  718. function __guard__(value, transform) {
  719. return typeof value !== 'undefined' && value !== null
  720. ? transform(value)
  721. : undefined
  722. }