Răsfoiți Sursa

Merge pull request #52 from sharelatex/bg-tikz-externalize

support for tikz externalize
Brian Gough 9 ani în urmă
părinte
comite
03d97b4127

+ 9 - 1
services/clsi/app/coffee/CompileManager.coffee

@@ -8,6 +8,7 @@ logger = require "logger-sharelatex"
 Metrics = require "./Metrics"
 Metrics = require "./Metrics"
 child_process = require "child_process"
 child_process = require "child_process"
 DraftModeManager = require "./DraftModeManager"
 DraftModeManager = require "./DraftModeManager"
+TikzManager = require "./TikzManager"
 fs = require("fs")
 fs = require("fs")
 fse = require "fs-extra"
 fse = require "fs-extra"
 os = require("os")
 os = require("os")
@@ -42,6 +43,12 @@ module.exports = CompileManager =
 				else
 				else
 					callback()
 					callback()
 
 
+			createTikzFileIfRequired = (callback) ->
+				if TikzManager.needsOutputFile(request.rootResourcePath, request.resources)
+					TikzManager.injectOutputFile compileDir, request.rootResourcePath, callback
+				else
+					callback()
+
 			# set up environment variables for chktex
 			# set up environment variables for chktex
 			env = {}
 			env = {}
 			# only run chktex on LaTeX files (not knitr .Rtex files or any others)
 			# 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'
 				if request.check is 'validate'
 					env['CHKTEX_VALIDATE'] =  1
 					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?
 				return callback(error) if error?
 				timer = new Metrics.Timer("run-compile")
 				timer = new Metrics.Timer("run-compile")
 				# find the image tag to log it as a metric, e.g. 2015.1 (convert . to - for graphite)
 				# find the image tag to log it as a metric, e.g. 2015.1 (convert . to - for graphite)

+ 19 - 14
services/clsi/app/coffee/ResourceWriter.coffee

@@ -50,6 +50,8 @@ module.exports = ResourceWriter =
 						should_delete = false
 						should_delete = false
 					if path == "output.pdf" or path == "output.dvi" or path == "output.log"
 					if path == "output.pdf" or path == "output.dvi" or path == "output.log"
 						should_delete = true
 						should_delete = true
+					if path == "output.tex" # created by TikzManager if present in output files
+						should_delete = true
 					if should_delete
 					if should_delete
 						jobs.push (callback) -> ResourceWriter._deleteFileIfNotDirectory Path.join(basePath, path), callback
 						jobs.push (callback) -> ResourceWriter._deleteFileIfNotDirectory Path.join(basePath, path), callback
 
 
@@ -73,19 +75,22 @@ module.exports = ResourceWriter =
 				callback()
 				callback()
 
 
 	_writeResourceToDisk: (project_id, resource, basePath, callback = (error) ->) ->
 	_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?
 			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)

+ 38 - 0
services/clsi/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

+ 23 - 2
services/clsi/test/unit/coffee/ResourceWriterTests.coffee

@@ -157,6 +157,27 @@ describe "ResourceWriter", ->
 					.calledWith(new Error("resource path is outside root directory"))
 					.calledWith(new Error("resource path is outside root directory"))
 					.should.equal true
 					.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

+ 66 - 0
services/clsi/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