PdfController.coffee 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356
  1. define [
  2. "base"
  3. "libs/latex-log-parser"
  4. "libs/bib-log-parser"
  5. ], (App, LogParser, BibLogParser) ->
  6. App.controller "PdfController", ($scope, $http, ide, $modal, synctex, event_tracking, localStorage) ->
  7. autoCompile = true
  8. # pdf.view = uncompiled | pdf | errors
  9. $scope.pdf.view = if $scope?.pdf?.url then 'pdf' else 'uncompiled'
  10. $scope.shouldShowLogs = false
  11. $scope.$on "project:joined", () ->
  12. return if !autoCompile
  13. autoCompile = false
  14. $scope.recompile(isAutoCompile: true)
  15. $scope.hasPremiumCompile = $scope.project.features.compileGroup == "priority"
  16. $scope.$on "pdf:error:display", () ->
  17. $scope.pdf.view = 'errors'
  18. $scope.pdf.renderingError = true
  19. $scope.draft = localStorage("draft:#{$scope.project_id}") or false
  20. $scope.$watch "draft", (new_value, old_value) ->
  21. if new_value? and old_value != new_value
  22. localStorage("draft:#{$scope.project_id}", new_value)
  23. sendCompileRequest = (options = {}) ->
  24. url = "/project/#{$scope.project_id}/compile"
  25. if options.isAutoCompile
  26. url += "?auto_compile=true"
  27. return $http.post url, {
  28. rootDoc_id: options.rootDocOverride_id or null
  29. draft: $scope.draft
  30. _csrf: window.csrfToken
  31. }
  32. parseCompileResponse = (response) ->
  33. # Reset everything
  34. $scope.pdf.error = false
  35. $scope.pdf.timedout = false
  36. $scope.pdf.failure = false
  37. $scope.pdf.uncompiled = false
  38. $scope.pdf.projectTooLarge = false
  39. $scope.pdf.url = null
  40. $scope.pdf.clsiMaintenance = false
  41. $scope.pdf.tooRecentlyCompiled = false
  42. if response.status == "timedout"
  43. $scope.pdf.view = 'errors'
  44. $scope.pdf.timedout = true
  45. else if response.status == "autocompile-backoff"
  46. $scope.pdf.view = 'errors'
  47. $scope.pdf.uncompiled = true
  48. else if response.status == "project-too-large"
  49. $scope.pdf.view = 'errors'
  50. $scope.pdf.projectTooLarge = true
  51. else if response.status == "failure"
  52. $scope.pdf.view = 'errors'
  53. $scope.pdf.failure = true
  54. fetchLogs()
  55. else if response.status == 'clsi-maintenance'
  56. $scope.pdf.view = 'errors'
  57. $scope.pdf.clsiMaintenance = true
  58. else if response.status == "too-recently-compiled"
  59. $scope.pdf.view = 'errors'
  60. $scope.pdf.tooRecentlyCompiled = true
  61. else if response.status == "success"
  62. $scope.pdf.view = 'pdf'
  63. $scope.shouldShowLogs = false
  64. # define the base url
  65. $scope.pdf.url = "/project/#{$scope.project_id}/output/output.pdf?cache_bust=#{Date.now()}"
  66. # add a query string parameter for the compile group
  67. if response.compileGroup?
  68. $scope.pdf.compileGroup = response.compileGroup
  69. $scope.pdf.url = $scope.pdf.url + "&compileGroup=#{$scope.pdf.compileGroup}"
  70. # make a cache to look up files by name
  71. fileByPath = {}
  72. for file in response.outputFiles
  73. fileByPath[file.path] = file
  74. # if the pdf file has a build number, pass it to the clsi
  75. if fileByPath['output.pdf']?.build?
  76. build = fileByPath['output.pdf'].build
  77. $scope.pdf.url = $scope.pdf.url + "&build=#{build}"
  78. fetchLogs(fileByPath['output.log'])
  79. IGNORE_FILES = ["output.fls", "output.fdb_latexmk"]
  80. $scope.pdf.outputFiles = []
  81. if !response.outputFiles?
  82. return
  83. for file in response.outputFiles
  84. if IGNORE_FILES.indexOf(file.path) == -1
  85. # Turn 'output.blg' into 'blg file'.
  86. if file.path.match(/^output\./)
  87. file.name = "#{file.path.replace(/^output\./, "")} file"
  88. else
  89. file.name = file.path
  90. $scope.pdf.outputFiles.push file
  91. fetchLogs = (outputFile) ->
  92. qs = if outputFile?.build? then "?build=#{outputFile.build}" else ""
  93. $http.get "/project/#{$scope.project_id}/output/output.log" + qs
  94. .success (log) ->
  95. #console.log ">>", log
  96. $scope.pdf.rawLog = log
  97. logEntries = LogParser.parse(log, ignoreDuplicates: true)
  98. #console.log ">>", logEntries
  99. $scope.pdf.logEntries = logEntries
  100. $scope.pdf.logEntries.all = logEntries.errors.concat(logEntries.warnings).concat(logEntries.typesetting)
  101. # # # #
  102. proceed = () ->
  103. $scope.pdf.logEntryAnnotations = {}
  104. for entry in logEntries.all
  105. if entry.file?
  106. entry.file = normalizeFilePath(entry.file)
  107. entity = ide.fileTreeManager.findEntityByPath(entry.file)
  108. if entity?
  109. $scope.pdf.logEntryAnnotations[entity.id] ||= []
  110. $scope.pdf.logEntryAnnotations[entity.id].push {
  111. row: entry.line - 1
  112. type: if entry.level == "error" then "error" else "warning"
  113. text: entry.message
  114. }
  115. # Get the biber log and parse it too
  116. $http.get "/project/#{$scope.project_id}/output/output.blg" + qs
  117. .success (log) ->
  118. window._s = $scope
  119. biberLogEntries = BibLogParser.parse(log, {})
  120. if $scope.pdf.logEntries
  121. entries = $scope.pdf.logEntries
  122. all = biberLogEntries.errors.concat(biberLogEntries.warnings)
  123. entries.all = entries.all.concat(all)
  124. entries.errors = entries.errors.concat(biberLogEntries.errors)
  125. entries.warnings = entries.warnings.concat(biberLogEntries.warnings)
  126. proceed()
  127. .error (e) ->
  128. # it's not an error for the output.blg file to not be present
  129. proceed()
  130. # # # #
  131. .error () ->
  132. $scope.pdf.logEntries = []
  133. $scope.pdf.rawLog = ""
  134. getRootDocOverride_id = () ->
  135. doc = ide.editorManager.getCurrentDocValue()
  136. return null if !doc?
  137. for line in doc.split("\n")
  138. match = line.match /^[^%]*\\documentclass/
  139. if match
  140. return ide.editorManager.getCurrentDocId()
  141. return null
  142. normalizeFilePath = (path) ->
  143. path = path.replace(/^(.*)\/compiles\/[0-9a-f]{24}\/(\.\/)?/, "")
  144. path = path.replace(/^\/compile\//, "")
  145. rootDocDirname = ide.fileTreeManager.getRootDocDirname()
  146. if rootDocDirname?
  147. path = path.replace(/^\.\//, rootDocDirname + "/")
  148. return path
  149. $scope.recompile = (options = {}) ->
  150. return if $scope.pdf.compiling
  151. $scope.pdf.compiling = true
  152. ide.$scope.$broadcast("flush-changes")
  153. options.rootDocOverride_id = getRootDocOverride_id()
  154. sendCompileRequest(options)
  155. .success (data) ->
  156. $scope.pdf.view = "pdf"
  157. $scope.pdf.compiling = false
  158. parseCompileResponse(data)
  159. .error () ->
  160. $scope.pdf.compiling = false
  161. $scope.pdf.error = true
  162. # This needs to be public.
  163. ide.$scope.recompile = $scope.recompile
  164. $scope.clearCache = () ->
  165. $http {
  166. url: "/project/#{$scope.project_id}/output"
  167. method: "DELETE"
  168. headers:
  169. "X-Csrf-Token": window.csrfToken
  170. }
  171. $scope.toggleLogs = () ->
  172. $scope.shouldShowLogs = !$scope.shouldShowLogs
  173. $scope.showPdf = () ->
  174. $scope.pdf.view = "pdf"
  175. $scope.shouldShowLogs = false
  176. $scope.toggleRawLog = () ->
  177. $scope.pdf.showRawLog = !$scope.pdf.showRawLog
  178. $scope.openClearCacheModal = () ->
  179. modalInstance = $modal.open(
  180. templateUrl: "clearCacheModalTemplate"
  181. controller: "ClearCacheModalController"
  182. scope: $scope
  183. )
  184. $scope.syncToCode = (position) ->
  185. synctex
  186. .syncToCode(position)
  187. .then (data) ->
  188. {doc, line} = data
  189. ide.editorManager.openDoc(doc, gotoLine: line)
  190. $scope.switchToFlatLayout = () ->
  191. $scope.ui.pdfLayout = 'flat'
  192. $scope.ui.view = 'pdf'
  193. ide.localStorage "pdf.layout", "flat"
  194. $scope.switchToSideBySideLayout = () ->
  195. $scope.ui.pdfLayout = 'sideBySide'
  196. $scope.ui.view = 'editor'
  197. localStorage "pdf.layout", "split"
  198. if pdfLayout = localStorage("pdf.layout")
  199. $scope.switchToSideBySideLayout() if pdfLayout == "split"
  200. $scope.switchToFlatLayout() if pdfLayout == "flat"
  201. else
  202. $scope.switchToSideBySideLayout()
  203. $scope.startFreeTrial = (source) ->
  204. ga?('send', 'event', 'subscription-funnel', 'compile-timeout', source)
  205. window.open("/user/subscription/new?planCode=student_free_trial_7_days")
  206. $scope.startedFreeTrial = true
  207. App.factory "synctex", ["ide", "$http", "$q", (ide, $http, $q) ->
  208. synctex =
  209. syncToPdf: (cursorPosition) ->
  210. deferred = $q.defer()
  211. doc_id = ide.editorManager.getCurrentDocId()
  212. if !doc_id?
  213. deferred.reject()
  214. return deferred.promise
  215. doc = ide.fileTreeManager.findEntityById(doc_id)
  216. if !doc?
  217. deferred.reject()
  218. return deferred.promise
  219. path = ide.fileTreeManager.getEntityPath(doc)
  220. if !path?
  221. deferred.reject()
  222. return deferred.promise
  223. # If the root file is folder/main.tex, then synctex sees the
  224. # path as folder/./main.tex
  225. rootDocDirname = ide.fileTreeManager.getRootDocDirname()
  226. if rootDocDirname? and rootDocDirname != ""
  227. path = path.replace(RegExp("^#{rootDocDirname}"), "#{rootDocDirname}/.")
  228. {row, column} = cursorPosition
  229. $http({
  230. url: "/project/#{ide.project_id}/sync/code",
  231. method: "GET",
  232. params: {
  233. file: path
  234. line: row + 1
  235. column: column
  236. }
  237. })
  238. .success (data) ->
  239. deferred.resolve(data.pdf or [])
  240. .error (error) ->
  241. deferred.reject(error)
  242. return deferred.promise
  243. syncToCode: (position, options = {}) ->
  244. deferred = $q.defer()
  245. if !position?
  246. deferred.reject()
  247. return deferred.promise
  248. # It's not clear exactly where we should sync to if it wasn't directly
  249. # clicked on, but a little bit down from the very top seems best.
  250. if options.includeVisualOffset
  251. position.offset.top = position.offset.top + 80
  252. $http({
  253. url: "/project/#{ide.project_id}/sync/pdf",
  254. method: "GET",
  255. params: {
  256. page: position.page + 1
  257. h: position.offset.left.toFixed(2)
  258. v: position.offset.top.toFixed(2)
  259. }
  260. })
  261. .success (data) ->
  262. if data.code? and data.code.length > 0
  263. doc = ide.fileTreeManager.findEntityByPath(data.code[0].file)
  264. return if !doc?
  265. deferred.resolve({doc: doc, line: data.code[0].line})
  266. .error (error) ->
  267. deferred.reject(error)
  268. return deferred.promise
  269. return synctex
  270. ]
  271. App.controller "PdfSynctexController", ["$scope", "synctex", "ide", ($scope, synctex, ide) ->
  272. @cursorPosition = null
  273. ide.$scope.$on "cursor:editor:update", (event, @cursorPosition) =>
  274. $scope.syncToPdf = () =>
  275. return if !@cursorPosition?
  276. synctex
  277. .syncToPdf(@cursorPosition)
  278. .then (highlights) ->
  279. $scope.pdf.highlights = highlights
  280. $scope.syncToCode = () ->
  281. synctex
  282. .syncToCode($scope.pdf.position, includeVisualOffset: true)
  283. .then (data) ->
  284. {doc, line} = data
  285. ide.editorManager.openDoc(doc, gotoLine: line)
  286. ]
  287. App.controller "PdfLogEntryController", ["$scope", "ide", ($scope, ide) ->
  288. $scope.openInEditor = (entry) ->
  289. entity = ide.fileTreeManager.findEntityByPath(entry.file)
  290. return if !entity? or entity.type != "doc"
  291. if entry.line?
  292. line = entry.line
  293. ide.editorManager.openDoc(entity, gotoLine: line)
  294. ]
  295. App.controller 'ClearCacheModalController', ["$scope", "$modalInstance", ($scope, $modalInstance) ->
  296. $scope.state =
  297. inflight: false
  298. $scope.clear = () ->
  299. $scope.state.inflight = true
  300. $scope
  301. .clearCache()
  302. .then () ->
  303. $scope.state.inflight = false
  304. $modalInstance.close()
  305. $scope.cancel = () ->
  306. $modalInstance.dismiss('cancel')
  307. ]