diff --git a/services/web/app/coffee/Features/Compile/ClsiManager.coffee b/services/web/app/coffee/Features/Compile/ClsiManager.coffee index 568d806e99..08ef23bcd8 100755 --- a/services/web/app/coffee/Features/Compile/ClsiManager.coffee +++ b/services/web/app/coffee/Features/Compile/ClsiManager.coffee @@ -147,6 +147,7 @@ module.exports = ClsiManager = timeout: options.timeout imageName: project.imageName draft: !!options.draft + check: options.check rootResourcePath: rootResourcePath resources: resources } diff --git a/services/web/app/coffee/Features/Compile/CompileController.coffee b/services/web/app/coffee/Features/Compile/CompileController.coffee index f3680dc38e..000465a83c 100755 --- a/services/web/app/coffee/Features/Compile/CompileController.coffee +++ b/services/web/app/coffee/Features/Compile/CompileController.coffee @@ -29,6 +29,8 @@ module.exports = CompileController = options.compiler = req.body.compiler if req.body?.draft options.draft = req.body.draft + if req.body?.check + options.check = if req.body.check is "validate" then "validate" else undefined logger.log {options:options, project_id:project_id, user_id:user_id}, "got compile request" CompileManager.compile project_id, user_id, options, (error, status, outputFiles, clsiServerId, limits, validationProblems) -> return next(error) if error? diff --git a/services/web/app/views/project/editor/pdf.jade b/services/web/app/views/project/editor/pdf.jade index 200c609d97..844f0f51dc 100644 --- a/services/web/app/views/project/editor/pdf.jade +++ b/services/web/app/views/project/editor/pdf.jade @@ -25,7 +25,7 @@ div.full-size.pdf(ng-controller="PdfController") dropdown-toggle ) span.caret - ul.dropdown-menu.dropdown-menu-right + ul.dropdown-menu.dropdown-menu-left li.dropdown-header #{translate("compile_mode")} li a(href, ng-click="draft = false") @@ -36,6 +36,13 @@ div.full-size.pdf(ng-controller="PdfController") i.fa.fa-fw(ng-class="{'fa-check': draft}") |  #{translate("fast")}  span.subdued [draft] + if user.betaProgram + li.dropdown-header #{translate("file_checks")} + li + a(href, ng-click="recompile({check:true})") + i.fa.fa-fw() + |  #{translate("run_syntax_check")} + span.beta-feature-badge a( href ng-click="stop()" @@ -115,7 +122,8 @@ div.full-size.pdf(ng-controller="PdfController") |   span(ng-show="entry.file") {{ entry.file }} span(ng-show="entry.line") , line {{ entry.line }} - p.entry-message(ng-show="entry.message") {{ entry.message }} + p.entry-message(ng-show="entry.message") + | {{ entry.type }} {{ entry.message }} .card.card-hint( ng-if="entry.humanReadableHint" stop-propagation="click" @@ -126,7 +134,7 @@ div.full-size.pdf(ng-controller="PdfController") ng-show="entry.humanReadableHint", ng-bind-html="wikiEnabled ? entry.humanReadableHint : stripHTMLFromString(entry.humanReadableHint)") .card-hint-actions.clearfix - .card-hint-ext-link(ng-if="wikiEnabled") + .card-hint-ext-link(ng-if="wikiEnabled && entry.extraInfoURL") a( ng-href="{{ entry.extraInfoURL }}", ng-click="trackLogHintsLearnMore()" diff --git a/services/web/public/coffee/ide/editor/EditorManager.coffee b/services/web/public/coffee/ide/editor/EditorManager.coffee index 54d56cac19..64cbe1d884 100644 --- a/services/web/public/coffee/ide/editor/EditorManager.coffee +++ b/services/web/public/coffee/ide/editor/EditorManager.coffee @@ -46,7 +46,7 @@ define [ done = () => if options.gotoLine? - @$scope.$broadcast "editor:gotoLine", options.gotoLine + @$scope.$broadcast "editor:gotoLine", options.gotoLine, options.gotoColumn if doc.id == @$scope.editor.open_doc_id and !options.forceReopen @$scope.$apply () => diff --git a/services/web/public/coffee/ide/editor/directives/aceEditor/cursor-position/CursorPositionManager.coffee b/services/web/public/coffee/ide/editor/directives/aceEditor/cursor-position/CursorPositionManager.coffee index 7019b213b5..6e238122e1 100644 --- a/services/web/public/coffee/ide/editor/directives/aceEditor/cursor-position/CursorPositionManager.coffee +++ b/services/web/public/coffee/ide/editor/directives/aceEditor/cursor-position/CursorPositionManager.coffee @@ -23,10 +23,10 @@ define [], () -> @storeCursorPosition(@editor.getSession()) @storeScrollTopPosition(@editor.getSession()) - @$scope.$on "#{@$scope.name}:gotoLine", (editor, value) => - if value? + @$scope.$on "#{@$scope.name}:gotoLine", (editor, line, column) => + if line? setTimeout () => - @gotoLine(value) + @gotoLine(line, column) , 10 # Hack: Must happen after @gotoStoredPosition storeScrollTopPosition: (session) -> @@ -53,6 +53,6 @@ define [], () -> @editor.getSession().setScrollTop(pos.scrollTop or 0) delete @ignoreCursorPositionChanges - gotoLine: (line) -> - @editor.gotoLine(line) - @editor.focus() \ No newline at end of file + gotoLine: (line, column) -> + @editor.gotoLine(line, column) + @editor.focus() diff --git a/services/web/public/coffee/ide/human-readable-logs/HumanReadableLogs.coffee b/services/web/public/coffee/ide/human-readable-logs/HumanReadableLogs.coffee index 747ca1ad08..e59e35a40c 100644 --- a/services/web/public/coffee/ide/human-readable-logs/HumanReadableLogs.coffee +++ b/services/web/public/coffee/ide/human-readable-logs/HumanReadableLogs.coffee @@ -3,7 +3,10 @@ define [ "ide/human-readable-logs/HumanReadableLogsRules" ], (LogParser, ruleset) -> parse : (rawLog, options) -> - parsedLogEntries = LogParser.parse(rawLog, options) + if typeof rawLog is 'string' + parsedLogEntries = LogParser.parse(rawLog, options) + else + parsedLogEntries = rawLog _getRule = (logMessage) -> return rule for rule in ruleset when rule.regexToMatch.test logMessage @@ -12,7 +15,12 @@ define [ ruleDetails = _getRule entry.message if (ruleDetails?) - entry.ruleId = 'hint_' + ruleDetails.regexToMatch.toString().replace(/\s/g, '_').slice(1, -1) if ruleDetails.regexToMatch? + if ruleDetails.ruleId? + entry.ruleId = ruleDetails.ruleId + else if ruleDetails.regexToMatch? + entry.ruleId = 'hint_' + ruleDetails.regexToMatch.toString().replace(/\s/g, '_').slice(1, -1) + if ruleDetails.newMessage? + entry.message = entry.message.replace ruleDetails.regexToMatch, ruleDetails.newMessage entry.humanReadableHint = ruleDetails.humanReadableHint if ruleDetails.humanReadableHint? entry.extraInfoURL = ruleDetails.extraInfoURL if ruleDetails.extraInfoURL? diff --git a/services/web/public/coffee/ide/human-readable-logs/HumanReadableLogsRules.coffee b/services/web/public/coffee/ide/human-readable-logs/HumanReadableLogsRules.coffee index f123515deb..c61c61506b 100644 --- a/services/web/public/coffee/ide/human-readable-logs/HumanReadableLogsRules.coffee +++ b/services/web/public/coffee/ide/human-readable-logs/HumanReadableLogsRules.coffee @@ -88,4 +88,19 @@ define -> [ humanReadableHint: """ You have used a font command which is only available in math mode. To use this command, you must be in maths mode (E.g. $ \u2026 $ or \\begin{math} \u2026 \\end{math}). If you want to use it outside of math mode, use the text version instead: \\textrm, \\textit, etc. """ + , + ruleId: "hint_mismatched_environment" + regexToMatch: /Error: `([^']{2,})' expected, found `([^']{2,})'.*/ + newMessage: "Error: environment does not match \\begin{$1} ... \\end{$2}" + humanReadableHint: """ + You have used \\begin{...} without a corresponding \\end{...}. + """ + , + ruleId: "hint_mismatched_brackets" + regexToMatch: /Error: `([^a-zA-Z0-9])' expected, found `([^a-zA-Z0-9])'.*/ + newMessage: "Error: brackets do not match, found '$2' instead of '$1'" + humanReadableHint: """ + You have used an open bracket without a corresponding close bracket. + """ + ] diff --git a/services/web/public/coffee/ide/pdf/controllers/PdfController.coffee b/services/web/public/coffee/ide/pdf/controllers/PdfController.coffee index 0f8134258c..780fda5aa3 100644 --- a/services/web/public/coffee/ide/pdf/controllers/PdfController.coffee +++ b/services/web/public/coffee/ide/pdf/controllers/PdfController.coffee @@ -86,11 +86,15 @@ define [ return $http.post url, { rootDoc_id: options.rootDocOverride_id or null draft: $scope.draft + check: if options.check then "validate" else null _csrf: window.csrfToken }, {params: params} parseCompileResponse = (response) -> + # keep last url + last_pdf_url = $scope.pdf.url + # Reset everything $scope.pdf.error = false $scope.pdf.timedout = false @@ -120,11 +124,23 @@ define [ if response.status == "timedout" $scope.pdf.view = 'errors' $scope.pdf.timedout = true - fetchLogs(fileByPath['output.log'], fileByPath['output.blg']) + fetchLogs(fileByPath) else if response.status == "terminated" $scope.pdf.view = 'errors' $scope.pdf.compileTerminated = true - fetchLogs(fileByPath['output.log'], fileByPath['output.blg']) + fetchLogs(fileByPath) + else if response.status in ["validation-fail", "validation-pass"] + $scope.pdf.view = 'pdf' + $scope.pdf.compileExited = true + $scope.pdf.url = last_pdf_url + $scope.shouldShowLogs = true + fetchLogs(fileByPath, { validation: true }) + else if response.status == "exited" + $scope.pdf.view = 'pdf' + $scope.pdf.compileExited = true + $scope.pdf.url = last_pdf_url + $scope.shouldShowLogs = true + fetchLogs(fileByPath) else if response.status == "autocompile-backoff" $scope.pdf.view = 'uncompiled' else if response.status == "project-too-large" @@ -134,7 +150,7 @@ define [ $scope.pdf.view = 'errors' $scope.pdf.failure = true $scope.shouldShowLogs = true - fetchLogs(fileByPath['output.log'], fileByPath['output.blg']) + fetchLogs(fileByPath) else if response.status == 'clsi-maintenance' $scope.pdf.view = 'errors' $scope.pdf.clsiMaintenance = true @@ -165,7 +181,7 @@ define [ qs.popupDownload = true $scope.pdf.downloadUrl = "/project/#{$scope.project_id}/output/output.pdf" + createQueryString(qs) - fetchLogs(fileByPath['output.log'], fileByPath['output.blg']) + fetchLogs(fileByPath) IGNORE_FILES = ["output.fls", "output.fdb_latexmk"] $scope.pdf.outputFiles = [] @@ -186,7 +202,13 @@ define [ } - fetchLogs = (logFile, blgFile) -> + fetchLogs = (fileByPath, options) -> + + if options?.validation + chktexFile = fileByPath['output.chktex'] + else + logFile = fileByPath['output.log'] + blgFile = fileByPath['output.blg'] getFile = (name, file) -> opts = @@ -213,6 +235,8 @@ define [ accumulateResults = (newEntries) -> for key in ['all', 'errors', 'warnings'] + if newEntries.type? + entry.type = newEntries.type for entry in newEntries[key] logEntries[key] = logEntries[key].concat newEntries[key] # use the parsers for each file type @@ -222,10 +246,24 @@ define [ all = [].concat errors, warnings, typesetting accumulateResults {all, errors, warnings} + processChkTex = (log) -> + errors = [] + warnings = [] + for line in log.split("\n") + if m = line.match /^(\S+):(\d+):(\d+): (Error|Warning): (.*)/ + result = { file:m[1], line:m[2], column:m[3], level:m[4].toLowerCase(), message: "#{m[4]}: #{m[5]}"} + if result.level is 'error' + errors.push result + else + warnings.push result + all = [].concat errors, warnings + logHints = HumanReadableLogs.parse {type: "Validation", all, errors, warnings} + accumulateResults logHints + processBiber = (log) -> {errors, warnings} = BibLogParser.parse(log, {}) all = [].concat errors, warnings - accumulateResults {all, errors, warnings} + accumulateResults {type: "BibTeX", all, errors, warnings} # output the results handleError = () -> @@ -248,19 +286,35 @@ define [ } # retrieve the logfile and process it - response = getFile('output.log', logFile) - .success processLog - .error handleError + if logFile? + response = getFile('output.log', logFile) + .then (response) -> processLog(response.data) - if blgFile? # retrieve the blg file if present - response.success () -> - getFile('output.blg', blgFile) - # ignore errors in biber file - .success processBiber - # display the combined result - .then annotateFiles - else # otherwise just display the result - response.success annotateFiles + if blgFile? # retrieve the blg file if present + response = response.then () -> + getFile('output.blg', blgFile) + .then( + (response) -> processBiber(response.data), + () -> true # ignore errors in biber file + ) + + if response? + response.catch handleError + else + handleError() + + if chktexFile? + getChkTex = () -> + getFile('output.chktex', chktexFile) + .then (response) -> processChkTex(response.data) + # always retrieve the chktex file if present + if response? + response = response.then getChkTex, getChkTex + else + response = getChkTex() + + # display the combined result + response.finally annotateFiles getRootDocOverride_id = () -> doc = ide.editorManager.getCurrentDocValue() @@ -504,7 +558,9 @@ define [ return if !entity? or entity.type != "doc" if entry.line? line = entry.line - ide.editorManager.openDoc(entity, gotoLine: line) + if entry.column? + column = entry.column + ide.editorManager.openDoc(entity, gotoLine: line, gotoColumn: column) ] App.controller 'ClearCacheModalController', ["$scope", "$modalInstance", ($scope, $modalInstance) -> diff --git a/services/web/test/UnitTests/coffee/Compile/ClsiManagerTests.coffee b/services/web/test/UnitTests/coffee/Compile/ClsiManagerTests.coffee index 2fb980b461..a3eeebcaaa 100644 --- a/services/web/test/UnitTests/coffee/Compile/ClsiManagerTests.coffee +++ b/services/web/test/UnitTests/coffee/Compile/ClsiManagerTests.coffee @@ -174,6 +174,7 @@ describe "ClsiManager", -> timeout : 100 imageName: @image draft: false + check: undefined rootResourcePath: "main.tex" resources: [{ path: "main.tex"