diff --git a/app/coffee/CompileManager.coffee b/app/coffee/CompileManager.coffee index 0a1ea9d..e4cb452 100644 --- a/app/coffee/CompileManager.coffee +++ b/app/coffee/CompileManager.coffee @@ -8,6 +8,7 @@ logger = require "logger-sharelatex" Metrics = require "./Metrics" child_process = require "child_process" DraftModeManager = require "./DraftModeManager" +TikzManager = require "./TikzManager" fs = require("fs") fse = require "fs-extra" os = require("os") @@ -42,6 +43,12 @@ module.exports = CompileManager = else callback() + createTikzFileIfRequired = (callback) -> + if TikzManager.needsOutputFile(request.rootResourcePath, request.resources) + TikzManager.injectOutputFile compileDir, request.rootResourcePath, callback + else + callback() + # set up environment variables for chktex env = {} # only run chktex on LaTeX files (not knitr .Rtex files or any others) @@ -54,7 +61,8 @@ module.exports = CompileManager = if request.check is 'validate' env['CHKTEX_VALIDATE'] = 1 - injectDraftModeIfRequired (error) -> + # apply a series of file modifications/creations for draft mode and tikz + async.series [injectDraftModeIfRequired, createTikzFileIfRequired], (error) -> return callback(error) if error? timer = new Metrics.Timer("run-compile") # find the image tag to log it as a metric, e.g. 2015.1 (convert . to - for graphite) diff --git a/app/coffee/ResourceWriter.coffee b/app/coffee/ResourceWriter.coffee index 16000cb..2bf6598 100644 --- a/app/coffee/ResourceWriter.coffee +++ b/app/coffee/ResourceWriter.coffee @@ -50,6 +50,8 @@ module.exports = ResourceWriter = should_delete = false if path == "output.pdf" or path == "output.dvi" or path == "output.log" should_delete = true + if path == "output.tex" # created by TikzManager if present in output files + should_delete = true if should_delete jobs.push (callback) -> ResourceWriter._deleteFileIfNotDirectory Path.join(basePath, path), callback @@ -73,19 +75,22 @@ module.exports = ResourceWriter = callback() _writeResourceToDisk: (project_id, resource, basePath, callback = (error) ->) -> - path = Path.normalize(Path.join(basePath, resource.path)) - if (path.slice(0, basePath.length) != basePath) - return callback new Error("resource path is outside root directory") - - mkdirp Path.dirname(path), (error) -> + ResourceWriter.checkPath basePath, resource.path, (error, path) -> return callback(error) if error? - # TODO: Don't overwrite file if it hasn't been modified - if resource.url? - UrlCache.downloadUrlToFile project_id, resource.url, path, resource.modified, (err)-> - if err? - logger.err err:err, project_id:project_id, path:path, resource_url:resource.url, modified:resource.modified, "error downloading file for resources" - callback() #try and continue compiling even if http resource can not be downloaded at this time - else - fs.writeFile path, resource.content, callback + mkdirp Path.dirname(path), (error) -> + return callback(error) if error? + # TODO: Don't overwrite file if it hasn't been modified + if resource.url? + UrlCache.downloadUrlToFile project_id, resource.url, path, resource.modified, (err)-> + if err? + logger.err err:err, project_id:project_id, path:path, resource_url:resource.url, modified:resource.modified, "error downloading file for resources" + callback() #try and continue compiling even if http resource can not be downloaded at this time + else + fs.writeFile path, resource.content, callback - + checkPath: (basePath, resourcePath, callback) -> + path = Path.normalize(Path.join(basePath, resourcePath)) + if (path.slice(0, basePath.length + 1) != basePath + "/") + return callback new Error("resource path is outside root directory") + else + return callback(null, path) diff --git a/app/coffee/TikzManager.coffee b/app/coffee/TikzManager.coffee new file mode 100644 index 0000000..cfa1332 --- /dev/null +++ b/app/coffee/TikzManager.coffee @@ -0,0 +1,38 @@ +fs = require "fs" +Path = require "path" +ResourceWriter = require "./ResourceWriter" +logger = require "logger-sharelatex" + +# for \tikzexternalize to work the main file needs to match the +# jobname. Since we set the -jobname to output, we have to create a +# copy of the main file as 'output.tex'. + +module.exports = TikzManager = + needsOutputFile: (rootResourcePath, resources) -> + # if there's already an output.tex file, we don't want to touch it + for resource in resources + if resource.path is "output.tex" + return false + # if there's no output.tex, see if we are using tikz/pgf in the main file + for resource in resources + if resource.path is rootResourcePath + return TikzManager._includesTikz (resource) + # otherwise false + return false + + _includesTikz: (resource) -> + # check if we are using tikz externalize + content = resource.content.slice(0,65536) + if content.indexOf("\\tikzexternalize") >= 0 + return true + else + return false + + injectOutputFile: (compileDir, mainFile, callback = (error) ->) -> + ResourceWriter.checkPath compileDir, mainFile, (error, path) -> + return callback(error) if error? + fs.readFile path, "utf8", (error, content) -> + return callback(error) if error? + logger.log compileDir: compileDir, mainFile: mainFile, "copied file to ouput.tex for tikz" + # use wx flag to ensure that output file does not already exist + fs.writeFile Path.join(compileDir, "output.tex"), content, {flag:'wx'}, callback diff --git a/test/unit/coffee/ResourceWriterTests.coffee b/test/unit/coffee/ResourceWriterTests.coffee index c1e72a8..96140c9 100644 --- a/test/unit/coffee/ResourceWriterTests.coffee +++ b/test/unit/coffee/ResourceWriterTests.coffee @@ -157,6 +157,27 @@ describe "ResourceWriter", -> .calledWith(new Error("resource path is outside root directory")) .should.equal true - + describe "checkPath", -> + describe "with a valid path", -> + beforeEach -> + @ResourceWriter.checkPath("foo", "bar", @callback) - + it "should return the joined path", -> + @callback.calledWith(null, "foo/bar") + .should.equal true + + describe "with an invalid path", -> + beforeEach -> + @ResourceWriter.checkPath("foo", "baz/../../bar", @callback) + + it "should return an error", -> + @callback.calledWith(new Error("resource path is outside root directory")) + .should.equal true + + describe "with another invalid path matching on a prefix", -> + beforeEach -> + @ResourceWriter.checkPath("foo", "../foobar/baz", @callback) + + it "should return an error", -> + @callback.calledWith(new Error("resource path is outside root directory")) + .should.equal true diff --git a/test/unit/coffee/TikzManager.coffee b/test/unit/coffee/TikzManager.coffee new file mode 100644 index 0000000..8174b4a --- /dev/null +++ b/test/unit/coffee/TikzManager.coffee @@ -0,0 +1,66 @@ +SandboxedModule = require('sandboxed-module') +sinon = require('sinon') +require('chai').should() +modulePath = require('path').join __dirname, '../../../app/js/TikzManager' + +describe 'TikzManager', -> + beforeEach -> + @TikzManager = SandboxedModule.require modulePath, requires: + "./ResourceWriter": @ResourceWriter = {} + "fs": @fs = {} + "logger-sharelatex": @logger = {log: () ->} + + describe "needsOutputFile", -> + it "should return true if there is a \\tikzexternalize", -> + @TikzManager.needsOutputFile("main.tex", [ + { path: 'foo.tex' }, + { path: 'main.tex', content:'foo \\usepackage{tikz} \\tikzexternalize' } + ]).should.equal true + + it "should return false if there is no \\tikzexternalize", -> + @TikzManager.needsOutputFile("main.tex", [ + { path: 'foo.tex' }, + { path: 'main.tex', content:'foo \\usepackage{tikz}' } + ]).should.equal false + + it "should return false if there is already an output.tex file", -> + @TikzManager.needsOutputFile("main.tex", [ + { path: 'foo.tex' }, + { path: 'main.tex', content:'foo \\usepackage{tikz} \\tikzexternalize' }, + { path: 'output.tex' } + ]).should.equal false + + describe "injectOutputFile", -> + beforeEach -> + @rootDir = "/mock" + @filename = "filename.tex" + @callback = sinon.stub() + @content = ''' + \\documentclass{article} + \\usepackage{tikz} + \\tikzexternalize + \\begin{document} + Hello world + \\end{document} + ''' + @fs.readFile = sinon.stub().callsArgWith(2, null, @content) + @fs.writeFile = sinon.stub().callsArg(3) + @ResourceWriter.checkPath = sinon.stub().callsArgWith(2, null, "#{@rootDir}/#{@filename}") + @TikzManager.injectOutputFile @rootDir, @filename, @callback + + it "sould check the path", -> + @ResourceWriter.checkPath.calledWith(@rootDir, @filename) + .should.equal true + + it "should read the file", -> + @fs.readFile + .calledWith("#{@rootDir}/#{@filename}", "utf8") + .should.equal true + + it "should write out the same file as output.tex", -> + @fs.writeFile + .calledWith("#{@rootDir}/output.tex", @content, {flag: 'wx'}) + .should.equal true + + it "should call the callback", -> + @callback.called.should.equal true