CompileManager.js 24 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761
  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(
  82. request,
  83. compileDir,
  84. function (error, resourceList) {
  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(
  276. 'run-compile-per-pass',
  277. ts / stats['latex-runs']
  278. )
  279. }
  280. if (
  281. (stats != null ? stats['latex-runs'] : undefined) > 0 &&
  282. (timings != null ? timings['cpu-time'] : undefined) > 0
  283. ) {
  284. Metrics.timing(
  285. 'run-compile-cpu-time-per-pass',
  286. timings['cpu-time'] / stats['latex-runs']
  287. )
  288. }
  289. // Emit compile time.
  290. timings.compile = ts
  291. timer = new Metrics.Timer('process-output-files')
  292. return OutputFileFinder.findOutputFiles(
  293. resourceList,
  294. compileDir,
  295. function (error, outputFiles) {
  296. if (error != null) {
  297. return callback(error)
  298. }
  299. return OutputCacheManager.saveOutputFiles(
  300. { request, stats, timings },
  301. outputFiles,
  302. compileDir,
  303. outputDir,
  304. (err, newOutputFiles) => {
  305. if (err) {
  306. const { project_id: projectId, user_id: userId } =
  307. request
  308. logger.err(
  309. { projectId, userId, err },
  310. 'failed to save output files'
  311. )
  312. }
  313. const outputStage = timer.done()
  314. timings.sync = syncStage
  315. timings.output = outputStage
  316. // Emit e2e compile time.
  317. timings.compileE2E = timerE2E.done()
  318. if (stats['pdf-size']) {
  319. emitPdfStats(stats, timings)
  320. }
  321. callback(null, newOutputFiles, stats, timings)
  322. }
  323. )
  324. }
  325. )
  326. }
  327. )
  328. }
  329. )
  330. }
  331. )
  332. },
  333. stopCompile(project_id, user_id, callback) {
  334. if (callback == null) {
  335. callback = function (error) {}
  336. }
  337. const compileName = getCompileName(project_id, user_id)
  338. return LatexRunner.killLatex(compileName, callback)
  339. },
  340. clearProject(project_id, user_id, _callback) {
  341. if (_callback == null) {
  342. _callback = function (error) {}
  343. }
  344. const callback = function (error) {
  345. _callback(error)
  346. return (_callback = function () {})
  347. }
  348. const compileDir = getCompileDir(project_id, user_id)
  349. const outputDir = getOutputDir(project_id, user_id)
  350. return CompileManager._checkDirectory(compileDir, function (err, exists) {
  351. if (err != null) {
  352. return callback(err)
  353. }
  354. if (!exists) {
  355. return callback()
  356. } // skip removal if no directory present
  357. const proc = child_process.spawn('rm', [
  358. '-r',
  359. '-f',
  360. '--',
  361. compileDir,
  362. outputDir,
  363. ])
  364. proc.on('error', callback)
  365. let stderr = ''
  366. proc.stderr.setEncoding('utf8').on('data', chunk => (stderr += chunk))
  367. return proc.on('close', function (code) {
  368. if (code === 0) {
  369. return callback(null)
  370. } else {
  371. return callback(
  372. new Error(`rm -r ${compileDir} ${outputDir} failed: ${stderr}`)
  373. )
  374. }
  375. })
  376. })
  377. },
  378. _findAllDirs(callback) {
  379. if (callback == null) {
  380. callback = function (error, allDirs) {}
  381. }
  382. const root = Settings.path.compilesDir
  383. return fs.readdir(root, function (err, files) {
  384. if (err != null) {
  385. return callback(err)
  386. }
  387. const allDirs = Array.from(files).map(file => Path.join(root, file))
  388. return callback(null, allDirs)
  389. })
  390. },
  391. clearExpiredProjects(max_cache_age_ms, callback) {
  392. if (callback == null) {
  393. callback = function (error) {}
  394. }
  395. const now = Date.now()
  396. // action for each directory
  397. const expireIfNeeded = (checkDir, cb) =>
  398. fs.stat(checkDir, function (err, stats) {
  399. if (err != null) {
  400. return cb()
  401. } // ignore errors checking directory
  402. const age = now - stats.mtime
  403. const hasExpired = age > max_cache_age_ms
  404. if (hasExpired) {
  405. return fse.remove(checkDir, cb)
  406. } else {
  407. return cb()
  408. }
  409. })
  410. // iterate over all project directories
  411. return CompileManager._findAllDirs(function (error, allDirs) {
  412. if (error != null) {
  413. return callback()
  414. }
  415. return async.eachSeries(allDirs, expireIfNeeded, callback)
  416. })
  417. },
  418. _checkDirectory(compileDir, callback) {
  419. if (callback == null) {
  420. callback = function (error, exists) {}
  421. }
  422. return fs.lstat(compileDir, function (err, stats) {
  423. if ((err != null ? err.code : undefined) === 'ENOENT') {
  424. return callback(null, false) // directory does not exist
  425. } else if (err != null) {
  426. logger.err(
  427. { dir: compileDir, err },
  428. 'error on stat of project directory for removal'
  429. )
  430. return callback(err)
  431. } else if (!(stats != null ? stats.isDirectory() : undefined)) {
  432. logger.err(
  433. { dir: compileDir, stats },
  434. 'bad project directory for removal'
  435. )
  436. return callback(new Error('project directory is not directory'))
  437. } else {
  438. return callback(null, true)
  439. }
  440. })
  441. }, // directory exists
  442. syncFromCode(
  443. project_id,
  444. user_id,
  445. file_name,
  446. line,
  447. column,
  448. imageName,
  449. callback
  450. ) {
  451. // If LaTeX was run in a virtual environment, the file path that synctex expects
  452. // might not match the file path on the host. The .synctex.gz file however, will be accessed
  453. // wherever it is on the host.
  454. if (callback == null) {
  455. callback = function (error, pdfPositions) {}
  456. }
  457. const compileName = getCompileName(project_id, user_id)
  458. const base_dir = Settings.path.synctexBaseDir(compileName)
  459. const file_path = base_dir + '/' + file_name
  460. const compileDir = getCompileDir(project_id, user_id)
  461. const synctex_path = `${base_dir}/output.pdf`
  462. const command = ['code', synctex_path, file_path, line, column]
  463. CompileManager._runSynctex(
  464. project_id,
  465. user_id,
  466. command,
  467. imageName,
  468. function (error, stdout) {
  469. if (error != null) {
  470. return callback(error)
  471. }
  472. logger.log(
  473. { project_id, user_id, file_name, line, column, command, stdout },
  474. 'synctex code output'
  475. )
  476. return callback(
  477. null,
  478. CompileManager._parseSynctexFromCodeOutput(stdout)
  479. )
  480. }
  481. )
  482. },
  483. syncFromPdf(project_id, user_id, page, h, v, imageName, callback) {
  484. if (callback == null) {
  485. callback = function (error, filePositions) {}
  486. }
  487. const compileName = getCompileName(project_id, user_id)
  488. const compileDir = getCompileDir(project_id, user_id)
  489. const base_dir = Settings.path.synctexBaseDir(compileName)
  490. const synctex_path = `${base_dir}/output.pdf`
  491. const command = ['pdf', synctex_path, page, h, v]
  492. CompileManager._runSynctex(
  493. project_id,
  494. user_id,
  495. command,
  496. imageName,
  497. function (error, stdout) {
  498. if (error != null) {
  499. return callback(error)
  500. }
  501. logger.log(
  502. { project_id, user_id, page, h, v, stdout },
  503. 'synctex pdf output'
  504. )
  505. return callback(
  506. null,
  507. CompileManager._parseSynctexFromPdfOutput(stdout, base_dir)
  508. )
  509. }
  510. )
  511. },
  512. _checkFileExists(dir, filename, callback) {
  513. if (callback == null) {
  514. callback = function (error) {}
  515. }
  516. const file = Path.join(dir, filename)
  517. return fs.stat(dir, function (error, stats) {
  518. if ((error != null ? error.code : undefined) === 'ENOENT') {
  519. return callback(new Errors.NotFoundError('no output directory'))
  520. }
  521. if (error != null) {
  522. return callback(error)
  523. }
  524. return fs.stat(file, function (error, stats) {
  525. if ((error != null ? error.code : undefined) === 'ENOENT') {
  526. return callback(new Errors.NotFoundError('no output file'))
  527. }
  528. if (error != null) {
  529. return callback(error)
  530. }
  531. if (!(stats != null ? stats.isFile() : undefined)) {
  532. return callback(new Error('not a file'))
  533. }
  534. return callback()
  535. })
  536. })
  537. },
  538. _runSynctex(project_id, user_id, command, imageName, callback) {
  539. if (callback == null) {
  540. callback = function (error, stdout) {}
  541. }
  542. const seconds = 1000
  543. command.unshift('/opt/synctex')
  544. const directory = getCompileDir(project_id, user_id)
  545. const timeout = 60 * 1000 // increased to allow for large projects
  546. const compileName = getCompileName(project_id, user_id)
  547. const compileGroup = 'synctex'
  548. CompileManager._checkFileExists(directory, 'output.synctex.gz', error => {
  549. if (error) {
  550. return callback(error)
  551. }
  552. return CommandRunner.run(
  553. compileName,
  554. command,
  555. directory,
  556. imageName ||
  557. (Settings.clsi && Settings.clsi.docker
  558. ? Settings.clsi.docker.image
  559. : undefined),
  560. timeout,
  561. {},
  562. compileGroup,
  563. function (error, output) {
  564. if (error != null) {
  565. logger.err(
  566. { err: error, command, project_id, user_id },
  567. 'error running synctex'
  568. )
  569. return callback(error)
  570. }
  571. return callback(null, output.stdout)
  572. }
  573. )
  574. })
  575. },
  576. _parseSynctexFromCodeOutput(output) {
  577. const results = []
  578. for (const line of Array.from(output.split('\n'))) {
  579. const [node, page, h, v, width, height] = Array.from(line.split('\t'))
  580. if (node === 'NODE') {
  581. results.push({
  582. page: parseInt(page, 10),
  583. h: parseFloat(h),
  584. v: parseFloat(v),
  585. height: parseFloat(height),
  586. width: parseFloat(width),
  587. })
  588. }
  589. }
  590. return results
  591. },
  592. _parseSynctexFromPdfOutput(output, base_dir) {
  593. const results = []
  594. for (let line of Array.from(output.split('\n'))) {
  595. let column, file_path, node
  596. ;[node, file_path, line, column] = Array.from(line.split('\t'))
  597. if (node === 'NODE') {
  598. const file = file_path.slice(base_dir.length + 1)
  599. results.push({
  600. file,
  601. line: parseInt(line, 10),
  602. column: parseInt(column, 10),
  603. })
  604. }
  605. }
  606. return results
  607. },
  608. wordcount(project_id, user_id, file_name, image, callback) {
  609. if (callback == null) {
  610. callback = function (error, pdfPositions) {}
  611. }
  612. logger.log({ project_id, user_id, file_name, image }, 'running wordcount')
  613. const file_path = `$COMPILE_DIR/${file_name}`
  614. const command = [
  615. 'texcount',
  616. '-nocol',
  617. '-inc',
  618. file_path,
  619. `-out=${file_path}.wc`,
  620. ]
  621. const compileDir = getCompileDir(project_id, user_id)
  622. const timeout = 60 * 1000
  623. const compileName = getCompileName(project_id, user_id)
  624. const compileGroup = 'wordcount'
  625. return fse.ensureDir(compileDir, function (error) {
  626. if (error != null) {
  627. logger.err(
  628. { error, project_id, user_id, file_name },
  629. 'error ensuring dir for sync from code'
  630. )
  631. return callback(error)
  632. }
  633. return CommandRunner.run(
  634. compileName,
  635. command,
  636. compileDir,
  637. image,
  638. timeout,
  639. {},
  640. compileGroup,
  641. function (error) {
  642. if (error != null) {
  643. return callback(error)
  644. }
  645. return fs.readFile(
  646. compileDir + '/' + file_name + '.wc',
  647. 'utf-8',
  648. function (err, stdout) {
  649. if (err != null) {
  650. // call it node_err so sentry doesn't use random path error as unique id so it can't be ignored
  651. logger.err(
  652. { node_err: err, command, compileDir, project_id, user_id },
  653. 'error reading word count output'
  654. )
  655. return callback(err)
  656. }
  657. const results = CompileManager._parseWordcountFromOutput(stdout)
  658. logger.log(
  659. { project_id, user_id, wordcount: results },
  660. 'word count results'
  661. )
  662. return callback(null, results)
  663. }
  664. )
  665. }
  666. )
  667. })
  668. },
  669. _parseWordcountFromOutput(output) {
  670. const results = {
  671. encode: '',
  672. textWords: 0,
  673. headWords: 0,
  674. outside: 0,
  675. headers: 0,
  676. elements: 0,
  677. mathInline: 0,
  678. mathDisplay: 0,
  679. errors: 0,
  680. messages: '',
  681. }
  682. for (const line of Array.from(output.split('\n'))) {
  683. const [data, info] = Array.from(line.split(':'))
  684. if (data.indexOf('Encoding') > -1) {
  685. results.encode = info.trim()
  686. }
  687. if (data.indexOf('in text') > -1) {
  688. results.textWords = parseInt(info, 10)
  689. }
  690. if (data.indexOf('in head') > -1) {
  691. results.headWords = parseInt(info, 10)
  692. }
  693. if (data.indexOf('outside') > -1) {
  694. results.outside = parseInt(info, 10)
  695. }
  696. if (data.indexOf('of head') > -1) {
  697. results.headers = parseInt(info, 10)
  698. }
  699. if (data.indexOf('Number of floats/tables/figures') > -1) {
  700. results.elements = parseInt(info, 10)
  701. }
  702. if (data.indexOf('Number of math inlines') > -1) {
  703. results.mathInline = parseInt(info, 10)
  704. }
  705. if (data.indexOf('Number of math displayed') > -1) {
  706. results.mathDisplay = parseInt(info, 10)
  707. }
  708. if (data === '(errors') {
  709. // errors reported as (errors:123)
  710. results.errors = parseInt(info, 10)
  711. }
  712. if (line.indexOf('!!! ') > -1) {
  713. // errors logged as !!! message !!!
  714. results.messages += line + '\n'
  715. }
  716. }
  717. return results
  718. },
  719. }
  720. function __guard__(value, transform) {
  721. return typeof value !== 'undefined' && value !== null
  722. ? transform(value)
  723. : undefined
  724. }