├── .appveyor.yml ├── .gitattributes ├── .github └── stale.yml ├── .gitignore ├── .travis.yml ├── CHANGELOG.md ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── LICENSE.md ├── README.md ├── keymaps └── latex.json ├── lib ├── build-state.js ├── builder-registry.js ├── builder.js ├── builders │ ├── knitr.js │ └── latexmk.js ├── composer.js ├── config-migrator.js ├── latex.js ├── logger.js ├── main.js ├── marker-manager.js ├── opener-registry.js ├── opener.js ├── openers │ ├── atril-opener.js │ ├── custom-opener.js │ ├── evince-opener.js │ ├── okular-opener.js │ ├── pdf-view-opener.js │ ├── preview-opener.js │ ├── qpdfview-opener.js │ ├── shell-open-opener.js │ ├── skim-opener.js │ ├── sumatra-opener.js │ ├── x-reader-opener.js │ ├── xdg-open-opener.js │ └── zathura-opener.js ├── parser.js ├── parsers │ ├── fdb-parser.js │ ├── log-parser.js │ └── magic-parser.js ├── process-manager.js ├── status-indicator.js ├── views │ ├── file-reference.js │ ├── log-dock.js │ ├── log-message.js │ ├── message-count.js │ ├── message-icon.js │ └── status-label.js └── werkzeug.js ├── menus └── latex.json ├── package.json ├── resources └── latexmkrc ├── script ├── cibuild ├── create-fixtures ├── install-ghostscript ├── install-knitr ├── install-miktex ├── install-texlive └── lint ├── spec ├── async-spec-helpers.js ├── build-registry-spec.js ├── builder-spec.js ├── builders │ ├── knitr-spec.js │ └── latexmk-spec.js ├── composer-spec.js ├── fixtures │ ├── _minted-wibble │ │ └── default.pygstyle │ ├── error-warning.tex │ ├── errors.log │ ├── file.fdb_latexmk │ ├── file.log │ ├── file.tex │ ├── filename with spaces.log │ ├── filename with spaces.tex │ ├── knitr │ │ └── file.Rnw │ ├── latexmk │ │ ├── asymptote-test.tex │ │ ├── glossaries-test.tex │ │ ├── index-test.tex │ │ ├── mpost-test.tex │ │ ├── nomencl-test.tex │ │ └── sagetex-test.tex │ ├── log-parse │ │ ├── file-dvi.fdb_latexmk │ │ ├── file-dvi.log │ │ ├── file-pdf.fdb_latexmk │ │ ├── file-pdf.log │ │ ├── file-pdfdvi.fdb_latexmk │ │ ├── file-pdfdvi.log │ │ ├── file-pdfps.fdb_latexmk │ │ ├── file-pdfps.log │ │ ├── file-ps.fdb_latexmk │ │ └── file-ps.log │ ├── magic-comments │ │ ├── multiple-magic-comments.tex │ │ ├── no-whitespace.tex │ │ ├── not-first-line.tex │ │ ├── override-settings.tex │ │ ├── override-settings.yaml │ │ └── root-comment.tex │ ├── master-tex-finder │ │ ├── multiple-masters │ │ │ ├── inc1.tex │ │ │ ├── inc2.tex │ │ │ ├── master1.tex │ │ │ └── master2.tex │ │ └── single-master │ │ │ ├── inc1.tex │ │ │ ├── inc2.tex │ │ │ ├── inc3.tex │ │ │ ├── master.log │ │ │ └── master.tex │ └── sub │ │ ├── foo bar.tex │ │ └── wibble.tex ├── latex-spec.js ├── logger-spec.js ├── marker-manager-spec.js ├── opener-registry-spec.js ├── parsers │ ├── fdb-parser-spec.js │ ├── log-parser-spec.js │ └── magic-parser-spec.js ├── process-manager-spec.js ├── spec-helpers.js └── stubs.js └── styles ├── latex.less └── markers.atom-text-editor.less /.appveyor.yml: -------------------------------------------------------------------------------- 1 | branches: 2 | only: 3 | - master 4 | 5 | skip_tags: true 6 | clone_depth: 1 7 | 8 | image: Visual Studio 2015 9 | 10 | install: 11 | - ps: Install-Product node 5 12 | 13 | environment: 14 | global: 15 | MIKTEX_HOME: '%PROGRAMFILES%\MiKTeX 2.9' 16 | R_HOME: '%PROGRAMFILES%\R' 17 | R_LIBS_USER: '%USERPROFILE%\R\library' 18 | APM_TEST_PACKAGES: "language-latex pdf-view" 19 | matrix: 20 | - ATOM_CHANNEL: stable 21 | - ATOM_CHANNEL: beta 22 | - TEX_DIST: miktex 23 | ATOM_CHANNEL: stable 24 | - TEX_DIST: miktex 25 | ATOM_CHANNEL: beta 26 | 27 | cache: 28 | - '%R_LIBS_USER%' 29 | 30 | build_script: 31 | - set PATH=C:\Program Files\Git\mingw64\bin;%PATH% 32 | - sh ./script/cibuild 33 | 34 | test: off 35 | deploy: off 36 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | latexmkrc linguist-language=Perl 2 | -------------------------------------------------------------------------------- /.github/stale.yml: -------------------------------------------------------------------------------- 1 | only: issues 2 | staleLabel: stale 3 | daysUntilStale: 90 4 | daysUntilClose: 14 5 | 6 | markComment: > 7 | This issue has been marked as stale because due to inactivity. 8 | It will be closed in 14 days if no further activity occurs. 9 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | npm-debug.log 3 | node_modules 4 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: generic 2 | 3 | branches: 4 | only: 5 | - master 6 | 7 | git: 8 | depth: 1 9 | 10 | os: 11 | - linux 12 | - osx 13 | 14 | sudo: false 15 | dist: trusty 16 | osx_image: xcode8 17 | 18 | env: 19 | global: 20 | - MAKE="make -j8" 21 | - R_LIBS_USER=${HOME}/R/library 22 | - APM_TEST_PACKAGES="language-latex pdf-view" 23 | matrix: 24 | - ATOM_CHANNEL=stable 25 | - ATOM_CHANNEL=beta 26 | 27 | addons: 28 | apt: 29 | sources: 30 | - ubuntu-toolchain-r-test 31 | - r-packages-trusty 32 | packages: 33 | - gcc-6 34 | - g++-6 35 | - r-recommended 36 | - freeglut3 37 | - ghostscript 38 | 39 | cache: 40 | directories: 41 | - ${R_LIBS_USER} 42 | 43 | before_script: 44 | - if [[ "${TRAVIS_OS_NAME:-}" == "linux" ]]; then export CC=gcc-6 CXX=g++-6; fi 45 | 46 | script: ./script/cibuild 47 | 48 | notifications: 49 | email: 50 | on_success: never 51 | on_failure: change 52 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | 2 | # Contributor Covenant Code of Conduct 3 | 4 | ## Our Pledge 5 | 6 | We as members, contributors, and leaders pledge to make participation in our 7 | community a harassment-free experience for everyone, regardless of age, body 8 | size, visible or invisible disability, ethnicity, sex characteristics, gender 9 | identity and expression, level of experience, education, socio-economic status, 10 | nationality, personal appearance, race, religion, or sexual identity 11 | and orientation. 12 | 13 | We pledge to act and interact in ways that contribute to an open, welcoming, 14 | diverse, inclusive, and healthy community. 15 | 16 | ## Our Standards 17 | 18 | Examples of behavior that contributes to a positive environment for our 19 | community include: 20 | 21 | * Demonstrating empathy and kindness toward other people 22 | * Being respectful of differing opinions, viewpoints, and experiences 23 | * Giving and gracefully accepting constructive feedback 24 | * Accepting responsibility and apologizing to those affected by our mistakes, 25 | and learning from the experience 26 | * Focusing on what is best not just for us as individuals, but for the 27 | overall community 28 | 29 | Examples of unacceptable behavior include: 30 | 31 | * The use of sexualized language or imagery, and sexual attention or 32 | advances of any kind 33 | * Trolling, insulting or derogatory comments, and personal or political attacks 34 | * Public or private harassment 35 | * Publishing others' private information, such as a physical or email 36 | address, without their explicit permission 37 | * Other conduct which could reasonably be considered inappropriate in a 38 | professional setting 39 | 40 | ## Enforcement Responsibilities 41 | 42 | Community leaders are responsible for clarifying and enforcing our standards of 43 | acceptable behavior and will take appropriate and fair corrective action in 44 | response to any behavior that they deem inappropriate, threatening, offensive, 45 | or harmful. 46 | 47 | Community leaders have the right and responsibility to remove, edit, or reject 48 | comments, commits, code, wiki edits, issues, and other contributions that are 49 | not aligned to this Code of Conduct, and will communicate reasons for moderation 50 | decisions when appropriate. 51 | 52 | ## Scope 53 | 54 | This Code of Conduct applies within all community spaces, and also applies when 55 | an individual is officially representing the community in public spaces. 56 | Examples of representing our community include using an official e-mail address, 57 | posting via an official social media account, or acting as an appointed 58 | representative at an online or offline event. 59 | 60 | ## Enforcement 61 | 62 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 63 | reported to the community leaders responsible for enforcement at 64 | [INSERT CONTACT METHOD]. 65 | All complaints will be reviewed and investigated promptly and fairly. 66 | 67 | All community leaders are obligated to respect the privacy and security of the 68 | reporter of any incident. 69 | 70 | ## Enforcement Guidelines 71 | 72 | Community leaders will follow these Community Impact Guidelines in determining 73 | the consequences for any action they deem in violation of this Code of Conduct: 74 | 75 | ### 1. Correction 76 | 77 | **Community Impact**: Use of inappropriate language or other behavior deemed 78 | unprofessional or unwelcome in the community. 79 | 80 | **Consequence**: A private, written warning from community leaders, providing 81 | clarity around the nature of the violation and an explanation of why the 82 | behavior was inappropriate. A public apology may be requested. 83 | 84 | ### 2. Warning 85 | 86 | **Community Impact**: A violation through a single incident or series 87 | of actions. 88 | 89 | **Consequence**: A warning with consequences for continued behavior. No 90 | interaction with the people involved, including unsolicited interaction with 91 | those enforcing the Code of Conduct, for a specified period of time. This 92 | includes avoiding interactions in community spaces as well as external channels 93 | like social media. Violating these terms may lead to a temporary or 94 | permanent ban. 95 | 96 | ### 3. Temporary Ban 97 | 98 | **Community Impact**: A serious violation of community standards, including 99 | sustained inappropriate behavior. 100 | 101 | **Consequence**: A temporary ban from any sort of interaction or public 102 | communication with the community for a specified period of time. No public or 103 | private interaction with the people involved, including unsolicited interaction 104 | with those enforcing the Code of Conduct, is allowed during this period. 105 | Violating these terms may lead to a permanent ban. 106 | 107 | ### 4. Permanent Ban 108 | 109 | **Community Impact**: Demonstrating a pattern of violation of community 110 | standards, including sustained inappropriate behavior, harassment of an 111 | individual, or aggression toward or disparagement of classes of individuals. 112 | 113 | **Consequence**: A permanent ban from any sort of public interaction within 114 | the community. 115 | 116 | ## Attribution 117 | 118 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], 119 | version 2.0, available at 120 | https://www.contributor-covenant.org/version/2/0/code_of_conduct.html. 121 | 122 | Community Impact Guidelines were inspired by [Mozilla's code of conduct 123 | enforcement ladder](https://github.com/mozilla/diversity). 124 | 125 | [homepage]: https://www.contributor-covenant.org 126 | 127 | For answers to common questions about this code of conduct, see the FAQ at 128 | https://www.contributor-covenant.org/faq. Translations are available at 129 | https://www.contributor-covenant.org/translations. 130 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contribution Guidelines 2 | First and foremost, thanks for contributing! I'm only one person and for this 3 | package to truly provide a first-class LaTeX experience from within Atom, your 4 | contributions are crucial. 5 | 6 | Before you set out on your adventure, please read this document carefully. It 7 | will save everyone involved both time and energy. 8 | 9 | ## Code of Conduct 10 | [![Contributor Covenant](https://img.shields.io/badge/Contributor%20Covenant-v2.0%20adopted-ff69b4.svg)](CODE_OF_CONDUCT.md) 11 | 12 | This project is governed by our [Code of Conduct](CODE_OF_CONDUCT.md). 13 | By participating, you are expected to uphold this code. 14 | 15 | ## Styleguides 16 | The short story is to use common sense and follow the existing styles and 17 | conventions, whether it's Git commit messages, JavaScript code, documentation, 18 | and so on. 19 | 20 | ### Git Commit Messages 21 | - The first line should be a short summary limited to 50 characters. 22 | - All other lines should be limited to 72 characters. 23 | - The first line and the (optional) detailed summary **must** always be 24 | separated by a single blank line since many tools rely on this convention. 25 | - Additional paragraphs should also be separated with blank lines, 26 | - Use the **present tense**; “Add feature” and not “Added feature”. 27 | - Use the **imperative mood**; “Fix bug …” and not “Fixes bug …”. 28 | - Reference issues and pull requests. If a commit resolves an issue, mention 29 | that in the summary, and prefix the reference with either “close”, “fix”, 30 | or “resolve” to automatically close the referenced issues when your pull 31 | requests are merged. 32 | 33 | See https://help.github.com/articles/closing-issues-via-commit-messages/. 34 | 35 | For additional details and inspiration, see [Tim Pope's excellent post][1] on 36 | commit message best practises. 37 | 38 | ### JavaScript Styleguide 39 | [![standard](https://cdn.rawgit.com/feross/standard/master/badge.svg)][2] 40 | 41 | This project follows the [JavaScript Standard Style][2]. Compliance with the 42 | rules is automatically checked during CI builds. If you want to check if your 43 | changes are adhering to the rules, simply execute `script/lint`. 44 | 45 | 46 | 47 | [1]: http://tbaggery.com/2008/04/19/a-note-about-git-commit-messages.html 48 | [2]: http://standardjs.com/ 49 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | Copyright (c) 2014 Thomas Johansen. 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining 4 | a copy of this software and associated documentation files (the 5 | "Software"), to deal in the Software without restriction, including 6 | without limitation the rights to use, copy, modify, merge, publish, 7 | distribute, sublicense, and/or sell copies of the Software, and to 8 | permit persons to whom the Software is furnished to do so, subject to 9 | the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be 12 | included in all copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 15 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 16 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 17 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 18 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 19 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 20 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /keymaps/latex.json: -------------------------------------------------------------------------------- 1 | { 2 | "atom-text-editor[data-grammar~=\"latex\"]": { 3 | "ctrl-alt-b": "latex:build", 4 | "ctrl-alt-s": "latex:sync", 5 | "ctrl-alt-c": "latex:clean" 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /lib/build-state.js: -------------------------------------------------------------------------------- 1 | /** @babel */ 2 | 3 | import path from 'path' 4 | import { isTexFile, isKnitrFile } from './werkzeug' 5 | 6 | function toArray (value) { 7 | return (typeof value === 'string') ? value.split(',').map(item => item.trim()) : Array.from(value) 8 | } 9 | 10 | function toBoolean (value) { 11 | return (typeof value === 'string') ? !!value.match(/^(true|yes)$/i) : !!value 12 | } 13 | 14 | class JobState { 15 | constructor (parent, jobName) { 16 | this.parent = parent 17 | this.jobName = jobName 18 | } 19 | 20 | getOutputFilePath () { 21 | return this.outputFilePath 22 | } 23 | 24 | setOutputFilePath (value) { 25 | this.outputFilePath = value 26 | } 27 | 28 | getFileDatabase () { 29 | return this.fileDatabase 30 | } 31 | 32 | setFileDatabase (value) { 33 | this.fileDatabase = value 34 | } 35 | 36 | getLogMessages () { 37 | return this.logMessages 38 | } 39 | 40 | setLogMessages (value) { 41 | this.logMessages = value 42 | } 43 | 44 | getJobName () { 45 | return this.jobName 46 | } 47 | 48 | getFilePath () { 49 | return this.parent.getFilePath() 50 | } 51 | 52 | getProjectPath () { 53 | return this.parent.getProjectPath() 54 | } 55 | 56 | getTexFilePath () { 57 | return this.parent.getTexFilePath() 58 | } 59 | 60 | setTexFilePath (value) { 61 | this.parent.setTexFilePath(value) 62 | } 63 | 64 | getKnitrFilePath () { 65 | return this.parent.getKnitrFilePath() 66 | } 67 | 68 | setKnitrFilePath (value) { 69 | this.parent.setKnitrFilePath(value) 70 | } 71 | 72 | getCleanPatterns () { 73 | return this.parent.getCleanPatterns() 74 | } 75 | 76 | getEnableSynctex () { 77 | return this.parent.getEnableSynctex() 78 | } 79 | 80 | getEnableShellEscape () { 81 | return this.parent.getEnableShellEscape() 82 | } 83 | 84 | getEnableExtendedBuildMode () { 85 | return this.parent.getEnableExtendedBuildMode() 86 | } 87 | 88 | getOpenResultAfterBuild () { 89 | return this.parent.getOpenResultAfterBuild() 90 | } 91 | 92 | getEngine () { 93 | return this.parent.getEngine() 94 | } 95 | 96 | getMoveResultToSourceDirectory () { 97 | return this.parent.getMoveResultToSourceDirectory() 98 | } 99 | 100 | getOutputDirectory () { 101 | return this.parent.getOutputDirectory() 102 | } 103 | 104 | getOutputFormat () { 105 | return this.parent.getOutputFormat() 106 | } 107 | 108 | getProducer () { 109 | return this.parent.getProducer() 110 | } 111 | 112 | getShouldRebuild () { 113 | return this.parent.getShouldRebuild() 114 | } 115 | } 116 | 117 | export default class BuildState { 118 | constructor (filePath, jobNames = [null], shouldRebuild = false) { 119 | this.setFilePath(filePath) 120 | this.setJobNames(jobNames) 121 | this.setShouldRebuild(shouldRebuild) 122 | this.setEnableSynctex(false) 123 | this.setEnableShellEscape(false) 124 | this.setEnableExtendedBuildMode(false) 125 | this.setOpenResultAfterBuild(false) 126 | this.subfiles = new Set() 127 | } 128 | 129 | getKnitrFilePath () { 130 | return this.knitrFilePath 131 | } 132 | 133 | setKnitrFilePath (value) { 134 | this.knitrFilePath = value 135 | } 136 | 137 | getTexFilePath () { 138 | return this.texFilePath 139 | } 140 | 141 | setTexFilePath (value) { 142 | this.texFilePath = value 143 | } 144 | 145 | getProjectPath () { 146 | return this.projectPath 147 | } 148 | 149 | setProjectPath (value) { 150 | this.projectPath = value 151 | } 152 | 153 | getCleanPatterns () { 154 | return this.cleanPatterns 155 | } 156 | 157 | setCleanPatterns (value) { 158 | this.cleanPatterns = toArray(value) 159 | } 160 | 161 | getEnableSynctex () { 162 | return this.enableSynctex 163 | } 164 | 165 | setEnableSynctex (value) { 166 | this.enableSynctex = toBoolean(value) 167 | } 168 | 169 | getEnableShellEscape () { 170 | return this.enableShellEscape 171 | } 172 | 173 | setEnableShellEscape (value) { 174 | this.enableShellEscape = toBoolean(value) 175 | } 176 | 177 | getEnableExtendedBuildMode () { 178 | return this.enableExtendedBuildMode 179 | } 180 | 181 | setEnableExtendedBuildMode (value) { 182 | this.enableExtendedBuildMode = toBoolean(value) 183 | } 184 | 185 | getOpenResultAfterBuild () { 186 | return this.openResultAfterBuild 187 | } 188 | 189 | setOpenResultAfterBuild (value) { 190 | this.openResultAfterBuild = toBoolean(value) 191 | } 192 | 193 | getEngine () { 194 | return this.engine 195 | } 196 | 197 | setEngine (value) { 198 | this.engine = value 199 | } 200 | 201 | getJobStates () { 202 | return this.jobStates 203 | } 204 | 205 | setJobStates (value) { 206 | this.jobStates = value 207 | } 208 | 209 | getMoveResultToSourceDirectory () { 210 | return this.moveResultToSourceDirectory 211 | } 212 | 213 | setMoveResultToSourceDirectory (value) { 214 | this.moveResultToSourceDirectory = toBoolean(value) 215 | } 216 | 217 | getOutputFormat () { 218 | return this.outputFormat 219 | } 220 | 221 | setOutputFormat (value) { 222 | this.outputFormat = value 223 | } 224 | 225 | getOutputDirectory () { 226 | return this.outputDirectory 227 | } 228 | 229 | setOutputDirectory (value) { 230 | this.outputDirectory = value 231 | } 232 | 233 | getProducer () { 234 | return this.producer 235 | } 236 | 237 | setProducer (value) { 238 | this.producer = value 239 | } 240 | 241 | getSubfiles () { 242 | return Array.from(this.subfiles.values()) 243 | } 244 | 245 | addSubfile (value) { 246 | this.subfiles.add(value) 247 | } 248 | 249 | hasSubfile (value) { 250 | return this.subfiles.has(value) 251 | } 252 | 253 | getShouldRebuild () { 254 | return this.shouldRebuild 255 | } 256 | 257 | setShouldRebuild (value) { 258 | this.shouldRebuild = toBoolean(value) 259 | } 260 | 261 | getFilePath () { 262 | return this.filePath 263 | } 264 | 265 | setFilePath (value) { 266 | this.filePath = value 267 | this.texFilePath = isTexFile(value) ? value : undefined 268 | this.knitrFilePath = isKnitrFile(value) ? value : undefined 269 | this.projectPath = path.dirname(value) 270 | } 271 | 272 | getJobNames () { 273 | return this.jobStates.map(jobState => jobState.getJobName()) 274 | } 275 | 276 | setJobNames (value) { 277 | this.jobStates = toArray(value).map(jobName => new JobState(this, jobName)) 278 | } 279 | } 280 | -------------------------------------------------------------------------------- /lib/builder-registry.js: -------------------------------------------------------------------------------- 1 | /** @babel */ 2 | 3 | import fs from 'fs-plus' 4 | import path from 'path' 5 | 6 | export default class BuilderRegistry { 7 | getBuilderImplementation (state) { 8 | const builders = this.getAllBuilders() 9 | const candidates = builders.filter(builder => builder.canProcess(state)) 10 | switch (candidates.length) { 11 | case 0: return null 12 | case 1: return candidates[0] 13 | } 14 | 15 | // This should never happen... 16 | throw new Error('Ambiguous builder registration.') 17 | } 18 | 19 | getBuilder (state) { 20 | const BuilderImpl = this.getBuilderImplementation(state) 21 | return (BuilderImpl != null) ? new BuilderImpl() : null 22 | } 23 | 24 | async checkRuntimeDependencies () { 25 | const builders = this.getAllBuilders() 26 | for (const BuilderImpl of builders) { 27 | const builder = new BuilderImpl() 28 | await builder.checkRuntimeDependencies() 29 | } 30 | } 31 | 32 | getAllBuilders () { 33 | const moduleDir = this.getModuleDirPath() 34 | const entries = fs.readdirSync(moduleDir) 35 | const builders = entries.map(entry => require(path.join(moduleDir, entry))) 36 | 37 | return builders 38 | } 39 | 40 | getModuleDirPath () { 41 | return path.join(__dirname, 'builders') 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /lib/builder.js: -------------------------------------------------------------------------------- 1 | /** @babel */ 2 | 3 | import fs from 'fs-plus' 4 | import path from 'path' 5 | import LogParser from './parsers/log-parser' 6 | import FdbParser from './parsers/fdb-parser' 7 | import { heredoc, isPdfFile, isPsFile, isDviFile } from './werkzeug.js' 8 | 9 | export default class Builder { 10 | envPathKey = this.getEnvironmentPathKey(process.platform) 11 | 12 | static canProcess (state) {} 13 | async run (jobState) {} 14 | constructArgs (jobState) {} 15 | async checkRuntimeDependencies () {} 16 | 17 | logStatusCode (statusCode, stderr) { 18 | switch (statusCode) { 19 | case 127: 20 | latex.log.error(heredoc(` 21 | TeXification failed! Builder executable '${this.executable}' not found. 22 | latex.texPath 23 | as configured: ${atom.config.get('latex.texPath')} 24 | when resolved: ${this.constructPath()} 25 | Make sure latex.texPath is configured correctly either adjust it \ 26 | via the settings view, or directly in your config.cson file. 27 | `)) 28 | break 29 | case 0: 30 | break 31 | default: 32 | const errorOutput = stderr ? ` and output of "${stderr}"` : '' 33 | latex.log.error(`TeXification failed with status code ${statusCode}${errorOutput}`) 34 | } 35 | } 36 | 37 | parseLogFile (jobState) { 38 | const logFilePath = this.resolveLogFilePath(jobState) 39 | if (fs.existsSync(logFilePath)) { 40 | let filePath = jobState.getTexFilePath() 41 | // Use main source path if the generated LaTeX file is missing. This will 42 | // enable log parsing and finding the project root to continue without the 43 | // generated LaTeX file. 44 | if (!filePath) filePath = jobState.getFilePath() 45 | const parser = this.getLogParser(logFilePath, filePath) 46 | const result = parser.parse() 47 | if (result) { 48 | if (result.messages) { 49 | jobState.setLogMessages(result.messages) 50 | } 51 | if (result.outputFilePath) { 52 | jobState.setOutputFilePath(result.outputFilePath) 53 | } 54 | } 55 | } 56 | } 57 | 58 | getLogParser (logFilePath, texFilePath) { 59 | return new LogParser(logFilePath, texFilePath) 60 | } 61 | 62 | constructChildProcessOptions (directoryPath, defaultEnv) { 63 | const env = Object.assign(defaultEnv || {}, process.env) 64 | const childPath = this.constructPath() 65 | if (childPath) { 66 | env[this.envPathKey] = childPath 67 | } 68 | 69 | return { 70 | allowKill: true, 71 | encoding: 'utf8', 72 | maxBuffer: 52428800, // Set process' max buffer size to 50 MB. 73 | cwd: directoryPath, // Run process with sensible CWD. 74 | env 75 | } 76 | } 77 | 78 | constructPath () { 79 | let texPath = (atom.config.get('latex.texPath') || '').trim() 80 | if (texPath.length === 0) { 81 | texPath = this.defaultTexPath(process.platform) 82 | } 83 | 84 | const processPath = process.env[this.envPathKey] 85 | const match = texPath.match(/^(.*)(\$PATH)(.*)$/) 86 | if (match) { 87 | return `${match[1]}${processPath}${match[3]}` 88 | } 89 | 90 | return [texPath, processPath] 91 | .filter(str => str && str.length > 0) 92 | .join(path.delimiter) 93 | } 94 | 95 | defaultTexPath (platform) { 96 | if (platform === 'win32') { 97 | return [ 98 | '%SystemDrive%\\texlive\\2017\\bin\\win32', 99 | '%SystemDrive%\\texlive\\2016\\bin\\win32', 100 | '%SystemDrive%\\texlive\\2015\\bin\\win32', 101 | '%ProgramFiles%\\MiKTeX 2.9\\miktex\\bin\\x64', 102 | '%ProgramFiles(x86)%\\MiKTeX 2.9\\miktex\\bin' 103 | ].join(';') 104 | } 105 | 106 | return [ 107 | '/usr/texbin', 108 | '/Library/TeX/texbin' 109 | ].join(':') 110 | } 111 | 112 | resolveOutputFilePath (jobState, ext) { 113 | let { dir, name } = path.parse(jobState.getFilePath()) 114 | if (jobState.getJobName()) { 115 | name = jobState.getJobName() 116 | } 117 | dir = path.resolve(dir, jobState.getOutputDirectory()) 118 | return path.format({ dir, name, ext }) 119 | } 120 | 121 | resolveLogFilePath (jobState) { 122 | return this.resolveOutputFilePath(jobState, '.log') 123 | } 124 | 125 | getEnvironmentPathKey (platform) { 126 | if (platform === 'win32') { return 'Path' } 127 | return 'PATH' 128 | } 129 | 130 | resolveFdbFilePath (jobState) { 131 | return this.resolveOutputFilePath(jobState, '.fdb_latexmk') 132 | } 133 | 134 | parseFdbFile (jobState) { 135 | const fdbFilePath = this.resolveFdbFilePath(jobState) 136 | if (fs.existsSync(fdbFilePath)) { 137 | const parser = this.getFdbParser(fdbFilePath) 138 | const result = parser.parse() 139 | if (result) { 140 | jobState.setFileDatabase(result) 141 | } 142 | } 143 | } 144 | 145 | getFdbParser (fdbFilePath) { 146 | return new FdbParser(fdbFilePath) 147 | } 148 | 149 | parseLogAndFdbFiles (jobState) { 150 | this.parseLogFile(jobState) 151 | this.parseFdbFile(jobState) 152 | 153 | const fdb = jobState.getFileDatabase() 154 | if (fdb) { 155 | const sections = ['ps2pdf', 'xdvipdfmx', 'dvipdf', 'dvips', 'latex', 'pdflatex', 'lualatex', 'xelatex'] 156 | let output 157 | 158 | for (const section of sections) { 159 | if (fdb[section] && fdb[section].generated) { 160 | const generated = fdb[section].generated 161 | 162 | output = generated.find(output => isPdfFile(output)) 163 | if (output) break 164 | 165 | output = generated.find(output => isPsFile(output)) 166 | if (output) break 167 | 168 | output = generated.find(output => isDviFile(output)) 169 | if (output) break 170 | } 171 | } 172 | 173 | if (output) { 174 | jobState.setOutputFilePath(path.resolve(jobState.getProjectPath(), path.normalize(output))) 175 | } 176 | } 177 | } 178 | } 179 | -------------------------------------------------------------------------------- /lib/builders/knitr.js: -------------------------------------------------------------------------------- 1 | /** @babel */ 2 | 3 | import path from 'path' 4 | import Builder from '../builder' 5 | 6 | const MISSING_PACKAGE_PATTERN = /there is no package called [‘']([^’']+)[’']/g 7 | const OUTPUT_PATH_PATTERN = /\[\d+]\s+"(.*)"/ 8 | const RSCRIPT_VERSION_PATTERN = /version\s+(\S+)/i 9 | const PACKAGE_VERSION_PATTERN = /^\[1] "([^"]*)"/ 10 | 11 | function escapePath (filePath) { 12 | return filePath.replace(/\\/g, '\\\\') 13 | } 14 | 15 | export default class KnitrBuilder extends Builder { 16 | executable = 'Rscript' 17 | 18 | static canProcess (state) { 19 | return !state.getTexFilePath() && !!state.getKnitrFilePath() 20 | } 21 | 22 | async run (jobState) { 23 | const args = this.constructArgs(jobState) 24 | const { statusCode, stdout, stderr } = await this.execRscript(jobState.getProjectPath(), args, 'error') 25 | if (statusCode !== 0) { 26 | this.logStatusCode(statusCode, stderr) 27 | return statusCode 28 | } 29 | 30 | jobState.setTexFilePath(this.resolveOutputPath(jobState.getKnitrFilePath(), stdout)) 31 | 32 | const builder = latex.builderRegistry.getBuilder(jobState) 33 | const code = await builder.run(jobState) 34 | 35 | if (code === 0 && jobState.getEnableSynctex()) { 36 | const args = this.constructPatchSynctexArgs(jobState) 37 | await this.execRscript(jobState.getProjectPath(), args, 'warning') 38 | } 39 | 40 | return code 41 | } 42 | 43 | async checkRuntimeDependencies () { 44 | const { statusCode, stderr } = await this.execRscript('.', ['--version'], 'warning') 45 | 46 | if (statusCode !== 0) { 47 | latex.log.warning(`Rscript check failed with code ${statusCode} and response of "${stderr}".`) 48 | return 49 | } 50 | 51 | const match = stderr.match(RSCRIPT_VERSION_PATTERN) 52 | 53 | if (!match) { 54 | latex.log.warning(`Rscript check succeeded but with an unknown version response of "${stderr}".`) 55 | return 56 | } 57 | 58 | const version = match[1] 59 | 60 | latex.log.info(`Rscript check succeeded. Found version ${version}.`) 61 | 62 | await this.checkRscriptPackageVersion('knitr') 63 | await this.checkRscriptPackageVersion('patchSynctex', '0.1-4') 64 | } 65 | 66 | async checkRscriptPackageVersion (packageName, minimumVersion) { 67 | const result = await this.execRscript('.', [`-e "installed.packages()['${packageName}','Version']"`], 'warning') 68 | 69 | if (result.statusCode === 0) { 70 | const match = result.stdout.match(PACKAGE_VERSION_PATTERN) 71 | if (match) { 72 | const version = match[1] 73 | const message = `Rscript ${packageName} package check succeeded. Found version ${version}.` 74 | if (minimumVersion && minimumVersion > version) { 75 | latex.log.warning(`${message} Minimum version ${minimumVersion} needed.`) 76 | } else { 77 | latex.log.info(message) 78 | } 79 | return 80 | } 81 | } 82 | 83 | latex.log.warning(`Rscript package ${packageName} was not found.`) 84 | } 85 | 86 | async execRscript (directoryPath, args, type) { 87 | const command = `${this.executable} ${args.join(' ')}` 88 | const options = this.constructChildProcessOptions(directoryPath) 89 | 90 | let { statusCode, stdout, stderr } = await latex.process.executeChildProcess(command, options) 91 | 92 | if (statusCode !== 0) { 93 | // Parse error message to detect missing libraries. 94 | let match 95 | while ((match = MISSING_PACKAGE_PATTERN.exec(stderr)) !== null) { 96 | const text = `The R package "${match[1]}" could not be loaded.` 97 | latex.log.showMessages([{ type, text }]) 98 | statusCode = -1 99 | } 100 | } 101 | 102 | return { statusCode, stdout, stderr } 103 | } 104 | 105 | constructArgs (jobState) { 106 | const args = [ 107 | '-e "library(knitr)"', 108 | '-e "opts_knit$set(concordance = TRUE)"', 109 | `-e "knit('${escapePath(jobState.getKnitrFilePath())}')"` 110 | ] 111 | 112 | return args 113 | } 114 | 115 | constructPatchSynctexArgs (jobState) { 116 | let synctexPath = this.resolveOutputFilePath(jobState, '') 117 | 118 | const args = [ 119 | '-e "library(patchSynctex)"', 120 | `-e "patchSynctex('${escapePath(jobState.getKnitrFilePath())}',syncfile='${escapePath(synctexPath)}')"` 121 | ] 122 | 123 | return args 124 | } 125 | 126 | resolveOutputPath (sourcePath, stdout) { 127 | const candidatePath = OUTPUT_PATH_PATTERN.exec(stdout)[1] 128 | if (path.isAbsolute(candidatePath)) { 129 | return candidatePath 130 | } 131 | 132 | const sourceDir = path.dirname(sourcePath) 133 | return path.join(sourceDir, candidatePath) 134 | } 135 | } 136 | -------------------------------------------------------------------------------- /lib/builders/latexmk.js: -------------------------------------------------------------------------------- 1 | /** @babel */ 2 | 3 | import path from 'path' 4 | import Builder from '../builder' 5 | 6 | const LATEX_PATTERN = /^latex|u?platex$/ 7 | const LATEXMK_VERSION_PATTERN = /Version\s+(\S+)/i 8 | const LATEXMK_MINIMUM_VERSION = '4.37' 9 | const PDF_ENGINE_PATTERN = /^(xelatex|lualatex)$/ 10 | 11 | export default class LatexmkBuilder extends Builder { 12 | executable = 'latexmk' 13 | 14 | static canProcess (state) { 15 | return !!state.getTexFilePath() 16 | } 17 | 18 | async run (jobState) { 19 | const args = this.constructArgs(jobState) 20 | 21 | const { statusCode, stderr } = await this.execLatexmk(jobState.getProjectPath(), args, 'error') 22 | if (statusCode !== 0) { 23 | this.logStatusCode(statusCode, stderr) 24 | } 25 | 26 | return statusCode 27 | } 28 | 29 | async execLatexmk (directoryPath, args, type) { 30 | const options = this.constructChildProcessOptions(directoryPath, { max_print_line: 1000 }) 31 | 32 | if (atom.config.get('latex.useRelativePaths') && options.cwd) { 33 | const absPath = args[args.length - 1].slice(1, -1) 34 | const relPath = path.relative(options.cwd, absPath) 35 | args[args.length - 1] = `"${relPath}"` 36 | } 37 | 38 | const command = `${this.executable} ${args.join(' ')}` 39 | 40 | return latex.process.executeChildProcess(command, options) 41 | } 42 | 43 | async checkRuntimeDependencies () { 44 | const { statusCode, stdout, stderr } = await this.execLatexmk('.', ['-v'], 'error') 45 | 46 | if (statusCode !== 0) { 47 | latex.log.error(`latexmk check failed with code ${statusCode} and response of "${stderr}".`) 48 | return 49 | } 50 | 51 | const match = stdout.match(LATEXMK_VERSION_PATTERN) 52 | 53 | if (!match) { 54 | latex.log.warning(`latexmk check succeeded but with an unknown version response of "${stdout}".`) 55 | return 56 | } 57 | 58 | const version = match[1] 59 | 60 | if (version < LATEXMK_MINIMUM_VERSION) { 61 | latex.log.warning(`latexmk check succeeded but with a version of ${version}". Minimum version required is ${LATEXMK_MINIMUM_VERSION}.`) 62 | return 63 | } 64 | 65 | latex.log.info(`latexmk check succeeded. Found version ${version}.`) 66 | } 67 | 68 | logStatusCode (statusCode, stderr) { 69 | switch (statusCode) { 70 | case 10: 71 | latex.log.error('latexmk: Bad command line arguments.') 72 | break 73 | case 11: 74 | latex.log.error('latexmk: File specified on command line not found or other file not found.') 75 | break 76 | case 12: 77 | latex.log.error('latexmk: Failure in some part of making files.') 78 | break 79 | case 13: 80 | latex.log.error('latexmk: error in initialization file.') 81 | break 82 | case 20: 83 | latex.log.error('latexmk: probable bug or retcode from called program.') 84 | break 85 | default: 86 | super.logStatusCode(statusCode, stderr) 87 | } 88 | } 89 | 90 | constructArgs (jobState) { 91 | const args = [ 92 | '-interaction=nonstopmode', 93 | '-f', 94 | '-cd', 95 | '-file-line-error' 96 | ] 97 | 98 | if (jobState.getShouldRebuild()) { 99 | args.push('-g') 100 | } 101 | if (jobState.getJobName()) { 102 | args.push(`-jobname="${jobState.getJobName()}"`) 103 | } 104 | if (jobState.getEnableShellEscape()) { 105 | args.push('-shell-escape') 106 | } 107 | if (jobState.getEnableSynctex()) { 108 | args.push('-synctex=1') 109 | } 110 | if (jobState.getEnableExtendedBuildMode()) { 111 | const latexmkrcPath = path.resolve(__dirname, '..', '..', 'resources', 'latexmkrc') 112 | args.push(`-r "${latexmkrcPath}"`) 113 | } 114 | 115 | if (jobState.getEngine().match(LATEX_PATTERN)) { 116 | args.push(`-latex="${jobState.getEngine()}"`) 117 | args.push(jobState.getOutputFormat() === 'pdf' 118 | ? this.constructPdfProducerArgs(jobState) 119 | : `-${jobState.getOutputFormat()}`) 120 | } else { 121 | // Look for other PDF engines that can be specified using short command 122 | // options, i.e. -lualatex and -xelatex 123 | if (jobState.getOutputFormat() === 'pdf' && jobState.getEngine().match(PDF_ENGINE_PATTERN)) { 124 | args.push(`-${jobState.getEngine()}`) 125 | } else { 126 | // Keep the option noise to a minimum by not passing default engine 127 | if (jobState.getEngine() !== 'pdflatex') { 128 | args.push(`-pdflatex="${jobState.getEngine()}"`) 129 | } 130 | args.push(`-${jobState.getOutputFormat()}`) 131 | } 132 | } 133 | 134 | if (jobState.getOutputDirectory()) { 135 | args.push(`-outdir="${jobState.getOutputDirectory()}"`) 136 | } 137 | 138 | args.push(`"${jobState.getTexFilePath()}"`) 139 | return args 140 | } 141 | 142 | constructPdfProducerArgs (jobState) { 143 | const producer = jobState.getProducer() 144 | 145 | switch (producer) { 146 | case 'ps2pdf': 147 | return '-pdfps' 148 | case 'dvipdf': 149 | return '-pdfdvi -e "$dvipdf = \'dvipdf %O %S %D\';"' 150 | default: 151 | return `-pdfdvi -e "$dvipdf = '${producer} %O -o %D %S';"` 152 | } 153 | } 154 | } 155 | -------------------------------------------------------------------------------- /lib/config-migrator.js: -------------------------------------------------------------------------------- 1 | /** @babel */ 2 | 3 | import { shell } from 'electron' 4 | import _ from 'lodash' 5 | import { heredoc } from './werkzeug' 6 | 7 | export default function checkConfigAndMigrate () { 8 | // TODO: remove after grace period 9 | checkCleanExtensions() 10 | checkOpenerSetting() 11 | checkMasterFileSearchSetting() 12 | checkBuilder() 13 | } 14 | 15 | function checkBuilder () { 16 | const builder = atom.config.get('latex.builder') 17 | if (!builder) return 18 | 19 | atom.config.unset('latex.builder') 20 | if (builder !== 'texify') return 21 | 22 | // -------------------------------------------------- 23 | // TODO: Remove this whole block after a grace period 24 | // -------------------------------------------------- 25 | const message = `LaTeX: The texify builder has been deprecated` 26 | const description = heredoc(` 27 | Support for the \`texify\` builder has been deprecated in favor of \`latexmk\`, 28 | and has been removed.`) 29 | 30 | const title = 'How to use latexmk with MiKTeX' 31 | const url = 'https://github.com/thomasjo/atom-latex/wiki/Using-latexmk-with-MiKTeX' 32 | const openUrl = (event) => { 33 | // NOTE: Horrible hack due to a bug in atom/notifications module... 34 | const element = event.target.parentElement.parentElement.parentElement.parentElement 35 | const notification = element.getModel() 36 | notification.dismiss() 37 | 38 | shell.openExternal(url) 39 | } 40 | 41 | atom.notifications.addWarning(message, { 42 | dismissable: true, description, buttons: [{ text: title, onDidClick: openUrl }] 43 | }) 44 | } 45 | 46 | function checkMasterFileSearchSetting () { 47 | if (!atom.config.get('latex.useMasterFileSearch')) return 48 | 49 | atom.config.unset('latex.useMasterFileSearch') 50 | 51 | const message = `LaTeX: The Master File Search setting has been deprecated` 52 | const description = heredoc(` 53 | Support for the Master File Search setting has been deprecated in favor of 54 | \`%!TEX root\` magic comments, and has been removed.`) 55 | atom.notifications.addInfo(message, { description }) 56 | } 57 | 58 | function checkCleanExtensions () { 59 | const cleanExtensions = atom.config.get('latex.cleanExtensions') 60 | if (!cleanExtensions) return 61 | 62 | let cleanPatterns = atom.config.get('latex.cleanPatterns') 63 | const defaultExtensions = [ 64 | '.aux', '.bbl', '.blg', '.fdb_latexmk', '.fls', '.lof', '.log', 65 | '.lol', '.lot', '.nav', '.out', '.pdf', '.snm', '.synctex.gz', '.toc' 66 | ] 67 | 68 | atom.config.unset('latex.cleanExtensions') 69 | 70 | const removedExtensions = _.difference(defaultExtensions, cleanExtensions) 71 | cleanPatterns = _.difference(cleanPatterns, removedExtensions.map(extension => `**/*${extension}`)) 72 | 73 | const addedExtensions = _.difference(cleanExtensions, defaultExtensions) 74 | cleanPatterns = _.union(cleanPatterns, addedExtensions.map(extension => `**/*${extension}`)) 75 | 76 | atom.config.set('latex.cleanPatterns', cleanPatterns) 77 | const message = 'LaTeX: The "latex:clean" command has changed' 78 | const description = heredoc(` 79 | Your custom extensions in the \`Clean Extensions\` settings have 80 | been migrated to the new setting \`Clean Patterns\`.`) 81 | atom.notifications.addInfo(message, { description }) 82 | } 83 | 84 | function checkOpenerSetting () { 85 | const alwaysOpenResultInAtom = atom.config.get('latex.alwaysOpenResultInAtom') 86 | if (!alwaysOpenResultInAtom) return 87 | 88 | atom.config.unset('latex.alwaysOpenResultInAtom') 89 | atom.config.set('latex.opener', 'pdf-view') 90 | } 91 | -------------------------------------------------------------------------------- /lib/latex.js: -------------------------------------------------------------------------------- 1 | /** @babel */ 2 | 3 | import Composer from './composer' 4 | import OpenerRegistry from './opener-registry' 5 | import ProcessManager from './process-manager' 6 | import StatusIndicator from './status-indicator' 7 | import BuilderRegistry from './builder-registry' 8 | import Logger from './logger' 9 | import { CompositeDisposable, Disposable } from 'atom' 10 | 11 | function defineImmutableProperty (obj, name, value) { 12 | if (Disposable.isDisposable(value)) { 13 | obj.disposables.add(value) 14 | } 15 | Object.defineProperty(obj, name, { value }) 16 | } 17 | 18 | export default class Latex extends Disposable { 19 | disposables = new CompositeDisposable() 20 | 21 | constructor () { 22 | super(() => this.disposables.dispose()) 23 | 24 | defineImmutableProperty(this, 'builderRegistry', new BuilderRegistry()) 25 | defineImmutableProperty(this, 'composer', new Composer()) 26 | defineImmutableProperty(this, 'log', new Logger()) 27 | defineImmutableProperty(this, 'opener', new OpenerRegistry()) 28 | defineImmutableProperty(this, 'process', new ProcessManager()) 29 | defineImmutableProperty(this, 'status', new StatusIndicator()) 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /lib/logger.js: -------------------------------------------------------------------------------- 1 | /** @babel */ 2 | 3 | import _ from 'lodash' 4 | import { CompositeDisposable, Disposable, Emitter } from 'atom' 5 | import { getEditorDetails } from './werkzeug' 6 | import LogDock from './views/log-dock' 7 | 8 | export default class Logger extends Disposable { 9 | disposables = new CompositeDisposable() 10 | emitter = new Emitter() 11 | 12 | constructor () { 13 | super(() => this.disposables.dispose()) 14 | this.loggingLevel = atom.config.get('latex.loggingLevel') 15 | this.disposables.add(atom.config.onDidChange('latex.loggingLevel', () => { 16 | this.loggingLevel = atom.config.get('latex.loggingLevel') 17 | this.refresh() 18 | })) 19 | this.disposables.add(this.emitter) 20 | this.disposables.add(atom.workspace.addOpener(uri => { 21 | if (uri === LogDock.LOG_DOCK_URI) { 22 | return new LogDock() 23 | } 24 | })) 25 | 26 | this.messages = [] 27 | } 28 | 29 | onMessages (callback) { 30 | return this.emitter.on('messages', callback) 31 | } 32 | 33 | error (text, filePath, range, logPath, logRange) { 34 | this.showMessages([{ type: 'error', text, filePath, range, logPath, logRange }]) 35 | } 36 | 37 | warning (text, filePath, range, logPath, logRange) { 38 | this.showMessages([{ type: 'warning', text, filePath, range, logPath, logRange }]) 39 | } 40 | 41 | info (text, filePath, range, logPath, logRange) { 42 | this.showMessages([{ type: 'info', text, filePath, range, logPath, logRange }]) 43 | } 44 | 45 | showMessages (messages) { 46 | messages = messages.map(message => _.pickBy(message)) 47 | this.messages = this.messages.concat(messages) 48 | 49 | const filteredMessages = messages.filter(message => this.messageTypeIsVisible(message.type)) 50 | 51 | if (filteredMessages.length > 0) { 52 | this.emitter.emit('messages', { messages: filteredMessages, reset: false }) 53 | } 54 | } 55 | 56 | clear () { 57 | this.messages = [] 58 | this.refresh() 59 | } 60 | 61 | refresh () { 62 | this.emitter.emit('messages', { messages: this.getMessages(), reset: true }) 63 | } 64 | 65 | getMessages (useFilters = true) { 66 | return useFilters 67 | ? this.messages.filter(message => this.messageTypeIsVisible(message.type)) 68 | : this.messages 69 | } 70 | 71 | setMessages (messages) { 72 | this.messages = messages 73 | this.emitter.emit('messages', { messages, reset: true }) 74 | } 75 | 76 | messageTypeIsVisible (type) { 77 | return type === 'error' || 78 | (this.loggingLevel !== 'error' && type === 'warning') || 79 | (this.loggingLevel === 'info' && type === 'info') 80 | } 81 | 82 | async sync () { 83 | // FIXME: There should be no direct interaction with editors. The required 84 | // values should be arguments. 85 | const { filePath, position } = getEditorDetails() 86 | if (filePath) { 87 | const logDock = await this.show() 88 | if (logDock) { 89 | logDock.update({ filePath, position }) 90 | } 91 | } 92 | } 93 | 94 | async toggle () { 95 | return atom.workspace.toggle(LogDock.LOG_DOCK_URI) 96 | } 97 | 98 | async show () { 99 | return atom.workspace.open(LogDock.LOG_DOCK_URI) 100 | } 101 | 102 | async hide () { 103 | return atom.workspace.hide(LogDock.LOG_DOCK_URI) 104 | } 105 | } 106 | -------------------------------------------------------------------------------- /lib/main.js: -------------------------------------------------------------------------------- 1 | /** @babel */ 2 | 3 | import { CompositeDisposable, Disposable } from 'atom' 4 | 5 | export default { 6 | activate (serialized) { 7 | this.bootstrap() 8 | 9 | if (serialized && serialized.messages) { 10 | latex.log.setMessages(serialized.messages) 11 | } 12 | 13 | this.disposables.add(atom.commands.add('atom-workspace', { 14 | 'latex:build': () => latex.composer.build(false), 15 | 'latex:check-runtime': () => this.checkRuntime(), 16 | 'latex:clean': () => latex.composer.clean(), 17 | 'latex:clear-log': () => latex.log.clear(), 18 | 'latex:hide-log': () => latex.log.hide(), 19 | 'latex:kill': () => latex.composer.kill(), 20 | 'latex:rebuild': () => latex.composer.build(true), 21 | 'latex:show-log': () => latex.log.show(), 22 | 'latex:sync-log': () => latex.log.sync(), 23 | 'latex:sync': () => latex.composer.sync(), 24 | 'latex:toggle-log': () => latex.log.toggle() 25 | })) 26 | 27 | this.disposables.add(atom.workspace.observeTextEditors(editor => { 28 | this.disposables.add(editor.onDidSave(() => { 29 | // Let's play it safe; only trigger builds for the active editor. 30 | const activeEditor = atom.workspace.getActiveTextEditor() 31 | if (editor === activeEditor && atom.config.get('latex.buildOnSave')) { 32 | latex.composer.build(false, false) 33 | } 34 | })) 35 | })) 36 | 37 | const MarkerManager = require('./marker-manager') 38 | this.disposables.add(atom.workspace.observeTextEditors(editor => { 39 | this.disposables.add(new MarkerManager(editor)) 40 | })) 41 | 42 | if (!atom.inSpecMode()) { 43 | const checkConfigAndMigrate = require('./config-migrator') 44 | checkConfigAndMigrate() 45 | } 46 | }, 47 | 48 | deactivate () { 49 | if (this.disposables) { 50 | this.disposables.dispose() 51 | delete this.disposables 52 | } 53 | 54 | delete global.latex 55 | }, 56 | 57 | serialize () { 58 | return { messages: latex.log.getMessages(false) } 59 | }, 60 | 61 | consumeStatusBar (statusBar) { 62 | this.bootstrap() 63 | latex.status.attachStatusBar(statusBar) 64 | return new Disposable(() => { 65 | if (global.latex) { 66 | global.latex.status.detachStatusBar() 67 | } 68 | }) 69 | }, 70 | 71 | deserializeLog (serialized) { 72 | this.bootstrap() 73 | const LogDock = require('./views/log-dock') 74 | return new LogDock() 75 | }, 76 | 77 | bootstrap () { 78 | if (!this.disposables) { 79 | this.disposables = new CompositeDisposable() 80 | } 81 | 82 | if (global.latex) { return } 83 | 84 | const Latex = require('./latex') 85 | global.latex = new Latex() 86 | this.disposables.add(global.latex) 87 | }, 88 | 89 | async checkRuntime () { 90 | // latex.log.group('LaTeX Check') 91 | latex.log.clear() 92 | await latex.builderRegistry.checkRuntimeDependencies() 93 | latex.opener.checkRuntimeDependencies() 94 | // latex.log.groupEnd() 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /lib/marker-manager.js: -------------------------------------------------------------------------------- 1 | /** @babel */ 2 | 3 | import { CompositeDisposable, Disposable } from 'atom' 4 | 5 | export default class MarkerManager extends Disposable { 6 | disposables = new CompositeDisposable() 7 | 8 | constructor (editor) { 9 | super(() => this.disposables.dispose()) 10 | 11 | this.editor = editor 12 | this.markers = [] 13 | 14 | this.disposables.add(latex.log.onMessages(({ messages, reset }) => this.addMarkers(messages, reset))) 15 | this.disposables.add(new Disposable(() => this.clear())) 16 | this.disposables.add(this.editor.onDidDestroy(() => this.dispose())) 17 | this.disposables.add(atom.config.onDidChange('latex.loggingLevel', () => this.update())) 18 | 19 | this.addMarkers(latex.log.getMessages()) 20 | } 21 | 22 | update () { 23 | this.addMarkers(latex.log.getMessages(), true) 24 | } 25 | 26 | addMarkers (messages, reset) { 27 | if (reset) this.clear() 28 | 29 | const editorPath = this.editor.getPath() 30 | const isVisible = (filePath, range) => filePath && range && editorPath.includes(filePath) 31 | 32 | if (editorPath) { 33 | for (const message of messages) { 34 | if (isVisible(message.filePath, message.range)) { 35 | this.addMarker(message.type, message.filePath, message.range) 36 | } 37 | if (isVisible(message.logPath, message.logRange)) { 38 | this.addMarker(message.type, message.logPath, message.logRange) 39 | } 40 | } 41 | } 42 | } 43 | 44 | addMarker (type, filePath, range) { 45 | const marker = this.editor.markBufferRange(range, { invalidate: 'touch' }) 46 | this.editor.decorateMarker(marker, { type: 'line-number', class: `latex-${type}` }) 47 | this.markers.push(marker) 48 | } 49 | 50 | clear () { 51 | for (const marker of this.markers) { 52 | marker.destroy() 53 | } 54 | this.markers = [] 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /lib/opener-registry.js: -------------------------------------------------------------------------------- 1 | /** @babel */ 2 | 3 | import _ from 'lodash' 4 | import path from 'path' 5 | import { CompositeDisposable, Disposable } from 'atom' 6 | 7 | export default class OpenerRegistry extends Disposable { 8 | openers = new Map() 9 | disposables = new CompositeDisposable() 10 | 11 | constructor () { 12 | super(() => this.disposables.dispose()) 13 | this.initializeOpeners() 14 | } 15 | 16 | initializeOpeners () { 17 | const schema = atom.config.getSchema('latex.opener') 18 | const dir = path.join(__dirname, 'openers') 19 | const ext = '.js' 20 | for (const openerName of schema.enum) { 21 | if (openerName !== 'automatic') { 22 | const name = `${openerName}-opener` 23 | const OpenerImpl = require(path.format({ dir, name, ext })) 24 | const opener = new OpenerImpl() 25 | this.disposables.add(opener) 26 | this.openers.set(openerName, opener) 27 | } 28 | } 29 | } 30 | 31 | checkRuntimeDependencies () { 32 | const pdfOpeners = Array.from(this.getCandidateOpeners('foo.pdf').keys()) 33 | if (pdfOpeners.length) { 34 | latex.log.info(`The following PDF capable openers were found: ${pdfOpeners.join(', ')}.`) 35 | } else { 36 | latex.log.error('No PDF capable openers were found.') 37 | } 38 | 39 | const psOpeners = Array.from(this.getCandidateOpeners('foo.ps').keys()) 40 | if (psOpeners.length) { 41 | latex.log.info(`The following PS capable openers were found: ${psOpeners.join(', ')}.`) 42 | } else { 43 | latex.log.warning('No PS capable openers were found.') 44 | } 45 | 46 | const dviOpeners = Array.from(this.getCandidateOpeners('foo.dvi').keys()) 47 | if (dviOpeners.length) { 48 | latex.log.info(`The following DVI capable openers were found: ${dviOpeners.join(', ')}.`) 49 | } else { 50 | latex.log.warning('No DVI capable openers were found.') 51 | } 52 | } 53 | 54 | async open (filePath, texPath, lineNumber) { 55 | const name = atom.config.get('latex.opener') 56 | let opener = this.openers.get(name) 57 | 58 | if (!opener || !opener.canOpen(filePath)) { 59 | opener = this.findOpener(filePath) 60 | } 61 | 62 | if (opener) { 63 | return opener.open(filePath, texPath, lineNumber) 64 | } else { 65 | latex.log.warning(`No opener found that can open ${filePath}.`) 66 | } 67 | } 68 | 69 | getCandidateOpeners (filePath) { 70 | const candidates = new Map() 71 | for (const [name, opener] of this.openers.entries()) { 72 | if (opener.canOpen(filePath)) candidates.set(name, opener) 73 | } 74 | return candidates 75 | } 76 | 77 | findOpener (filePath) { 78 | const openResultInBackground = atom.config.get('latex.openResultInBackground') 79 | const enableSynctex = atom.config.get('latex.enableSynctex') 80 | const candidates = Array.from(this.getCandidateOpeners(filePath).values()) 81 | 82 | if (!candidates.length) return 83 | 84 | const rankedCandidates = _.orderBy(candidates, 85 | [opener => opener.hasSynctex(), opener => opener.canOpenInBackground()], 86 | ['desc', 'desc']) 87 | 88 | if (enableSynctex) { 89 | // If the user wants openResultInBackground also and there is an opener 90 | // that supports that and SyncTeX it will be the first one because of 91 | // the priority sort. 92 | const opener = rankedCandidates.find(opener => opener.hasSynctex()) 93 | if (opener) return opener 94 | } 95 | 96 | if (openResultInBackground) { 97 | const opener = rankedCandidates.find(opener => opener.canOpenInBackground()) 98 | if (opener) return opener 99 | } 100 | 101 | return rankedCandidates[0] 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /lib/opener.js: -------------------------------------------------------------------------------- 1 | /** @babel */ 2 | 3 | import { Disposable } from 'atom' 4 | 5 | export default class Opener extends Disposable { 6 | async open (filePath, texPath, lineNumber) {} 7 | 8 | shouldOpenInBackground () { 9 | return atom.config.get('latex.openResultInBackground') 10 | } 11 | 12 | canOpen (filePath) { 13 | return false 14 | } 15 | 16 | hasSynctex () { 17 | return false 18 | } 19 | 20 | canOpenInBackground () { 21 | return false 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /lib/openers/atril-opener.js: -------------------------------------------------------------------------------- 1 | /** @babel */ 2 | 3 | import EvinceOpener from './evince-opener' 4 | 5 | const DBUS_NAMES = { 6 | applicationObject: '/org/mate/atril/Atril', 7 | applicationInterface: 'org.mate.atril.Application', 8 | 9 | daemonService: 'org.mate.atril.Daemon', 10 | daemonObject: '/org/mate/atril/Daemon', 11 | daemonInterface: 'org.mate.atril.Daemon', 12 | 13 | windowInterface: 'org.mate.atril.Window' 14 | } 15 | 16 | export default class AtrilOpener extends EvinceOpener { 17 | constructor () { 18 | super('Atril', DBUS_NAMES) 19 | } 20 | 21 | canOpenInBackground () { 22 | return false 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /lib/openers/custom-opener.js: -------------------------------------------------------------------------------- 1 | /** @babel */ 2 | 3 | import fs from 'fs-plus' 4 | import Opener from '../opener' 5 | import { isPdfFile } from '../werkzeug' 6 | 7 | export default class CustomOpener extends Opener { 8 | // Custom PDF viewer does not support texPath and lineNumber. 9 | async open (filePath, texPath, lineNumber) { 10 | const command = `"${atom.config.get('latex.viewerPath')}" "${filePath}"` 11 | 12 | await latex.process.executeChildProcess(command, { showError: true }) 13 | } 14 | 15 | canOpen (filePath) { 16 | return isPdfFile(filePath) && fs.existsSync(atom.config.get('latex.viewerPath')) 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /lib/openers/evince-opener.js: -------------------------------------------------------------------------------- 1 | /** @babel */ 2 | 3 | import Opener from '../opener' 4 | import { pathToUri, uriToPath } from '../werkzeug' 5 | 6 | const DBUS_NAMES = { 7 | applicationObject: '/org/gnome/evince/Evince', 8 | applicationInterface: 'org.gnome.evince.Application', 9 | 10 | daemonService: 'org.gnome.evince.Daemon', 11 | daemonObject: '/org/gnome/evince/Daemon', 12 | daemonInterface: 'org.gnome.evince.Daemon', 13 | 14 | windowInterface: 'org.gnome.evince.Window', 15 | 16 | fdApplicationObject: '/org/gtk/Application/anonymous', 17 | fdApplicationInterface: 'org.freedesktop.Application' 18 | } 19 | 20 | function syncSource (uri, point) { 21 | const filePath = uriToPath(uri) 22 | atom.focus() 23 | atom.workspace.open(filePath).then(editor => editor.setCursorBufferPosition(point)) 24 | } 25 | 26 | export default class EvinceOpener extends Opener { 27 | windows = new Map() 28 | 29 | constructor (name = 'Evince', dbusNames = DBUS_NAMES) { 30 | super(() => { 31 | for (const filePath of Array.from(this.windows.keys())) { 32 | this.disposeWindow(filePath) 33 | } 34 | }) 35 | this.name = name 36 | this.dbusNames = dbusNames 37 | this.initialize() 38 | } 39 | 40 | async initialize () { 41 | try { 42 | if (process.platform === 'linux') { 43 | const dbus = require('dbus-native') 44 | this.bus = dbus.sessionBus() 45 | this.daemon = await this.getInterface(this.dbusNames.daemonService, this.dbusNames.daemonObject, this.dbusNames.daemonInterface) 46 | } 47 | } catch (e) {} 48 | } 49 | 50 | async getWindow (filePath, texPath) { 51 | if (this.windows.has(filePath)) { 52 | return this.windows.get(filePath) 53 | } 54 | 55 | // First find the internal document name 56 | const documentName = await this.findDocument(filePath) 57 | 58 | // Get the application interface and get the window list of the application 59 | const evinceApplication = await this.getInterface(documentName, this.dbusNames.applicationObject, this.dbusNames.applicationInterface) 60 | const windowNames = await this.getWindowList(evinceApplication) 61 | 62 | // Get the window interface of the of the first (only) window 63 | const onClosed = () => this.disposeWindow(filePath) 64 | const windowInstance = { 65 | evinceWindow: await this.getInterface(documentName, windowNames[0], this.dbusNames.windowInterface), 66 | onClosed 67 | } 68 | 69 | if (this.dbusNames.fdApplicationObject) { 70 | // Get the GTK/FreeDesktop application interface so we can activate the window 71 | windowInstance.fdApplication = await this.getInterface(documentName, this.dbusNames.fdApplicationObject, this.dbusNames.fdApplicationInterface) 72 | } 73 | 74 | windowInstance.evinceWindow.on('SyncSource', syncSource) 75 | windowInstance.evinceWindow.on('Closed', windowInstance.onClosed) 76 | this.windows.set(filePath, windowInstance) 77 | 78 | // This seems to help with future syncs 79 | await this.syncView(windowInstance.evinceWindow, texPath, [0, 0], 0) 80 | 81 | return windowInstance 82 | } 83 | 84 | disposeWindow (filePath) { 85 | const windowInstance = this.windows.get(filePath) 86 | if (windowInstance) { 87 | windowInstance.evinceWindow.removeListener('SyncSource', syncSource) 88 | windowInstance.evinceWindow.removeListener('Closed', windowInstance.onClosed) 89 | this.windows.delete(filePath) 90 | } 91 | } 92 | 93 | async open (filePath, texPath, lineNumber) { 94 | try { 95 | const windowInstance = await this.getWindow(filePath, texPath) 96 | if (!this.shouldOpenInBackground() && windowInstance.fdApplication) { 97 | windowInstance.fdApplication.Activate({}) 98 | } 99 | 100 | // SyncView seems to want to activate the window sometimes 101 | await this.syncView(windowInstance.evinceWindow, texPath, [lineNumber, 0], 0) 102 | 103 | return true 104 | } catch (error) { 105 | latex.log.error(`An error occured while trying to run ${this.name} opener`) 106 | return false 107 | } 108 | } 109 | 110 | canOpen (filePath) { 111 | return !!this.daemon 112 | } 113 | 114 | hasSynctex () { 115 | return true 116 | } 117 | 118 | canOpenInBackground () { 119 | return true 120 | } 121 | 122 | getInterface (serviceName, objectPath, interfaceName) { 123 | return new Promise((resolve, reject) => { 124 | this.bus.getInterface(serviceName, objectPath, interfaceName, (error, interfaceInstance) => { 125 | if (error) { 126 | reject(error) 127 | } else { 128 | resolve(interfaceInstance) 129 | } 130 | }) 131 | }) 132 | } 133 | 134 | getWindowList (evinceApplication) { 135 | return new Promise((resolve, reject) => { 136 | evinceApplication.GetWindowList((error, windowNames) => { 137 | if (error) { 138 | reject(error) 139 | } else { 140 | resolve(windowNames) 141 | } 142 | }) 143 | }) 144 | } 145 | 146 | syncView (evinceWindow, source, point, timestamp) { 147 | return new Promise((resolve, reject) => { 148 | evinceWindow.SyncView(source, point, timestamp, (error) => { 149 | if (error) { 150 | reject(error) 151 | } else { 152 | resolve() 153 | } 154 | }) 155 | }) 156 | } 157 | 158 | findDocument (filePath) { 159 | return new Promise((resolve, reject) => { 160 | const uri = pathToUri(filePath) 161 | 162 | this.daemon.FindDocument(uri, true, (error, documentName) => { 163 | if (error) { 164 | reject(error) 165 | } else { 166 | resolve(documentName) 167 | } 168 | }) 169 | }) 170 | } 171 | } 172 | -------------------------------------------------------------------------------- /lib/openers/okular-opener.js: -------------------------------------------------------------------------------- 1 | /** @babel */ 2 | 3 | import fs from 'fs-plus' 4 | import { pathToUri } from '../werkzeug' 5 | import Opener from '../opener' 6 | 7 | export default class OkularOpener extends Opener { 8 | async open (filePath, texPath, lineNumber) { 9 | const okularPath = atom.config.get('latex.okularPath') 10 | const uri = pathToUri(filePath, `src:${lineNumber} ${texPath}`) 11 | const args = [ 12 | '--unique', 13 | `"${uri}"` 14 | ] 15 | if (this.shouldOpenInBackground()) args.unshift('--noraise') 16 | 17 | const command = `"${okularPath}" ${args.join(' ')}` 18 | 19 | await latex.process.executeChildProcess(command, { showError: true }) 20 | } 21 | 22 | canOpen (filePath) { 23 | return process.platform === 'linux' && fs.existsSync(atom.config.get('latex.okularPath')) 24 | } 25 | 26 | hasSynctex () { 27 | return true 28 | } 29 | 30 | canOpenInBackground () { 31 | return true 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /lib/openers/pdf-view-opener.js: -------------------------------------------------------------------------------- 1 | /** @babel */ 2 | 3 | import Opener from '../opener' 4 | import { isPdfFile } from '../werkzeug' 5 | 6 | export default class PdfViewOpener extends Opener { 7 | async open (filePath, texPath, lineNumber) { 8 | const texPane = atom.workspace.paneForURI(texPath) 9 | const previousActivePane = atom.workspace.getActivePane() 10 | 11 | // This prevents splitting the right pane multiple times 12 | if (texPane) { 13 | texPane.activate() 14 | } 15 | 16 | const options = { 17 | searchAllPanes: true, 18 | split: atom.config.get('latex.pdfViewSplitDirection') 19 | } 20 | 21 | const item = await atom.workspace.open(filePath, options) 22 | if (item && item.forwardSync) { 23 | item.forwardSync(texPath, lineNumber) 24 | } 25 | 26 | if (previousActivePane && this.shouldOpenInBackground()) { 27 | previousActivePane.activate() 28 | } 29 | 30 | return true 31 | } 32 | 33 | canOpen (filePath) { 34 | return isPdfFile(filePath) && atom.packages.isPackageActive('pdf-view') 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /lib/openers/preview-opener.js: -------------------------------------------------------------------------------- 1 | /** @babel */ 2 | 3 | import Opener from '../opener' 4 | import { isPdfFile, isPsFile } from '../werkzeug' 5 | 6 | export default class PreviewOpener extends Opener { 7 | async open (filePath, texPath, lineNumber) { 8 | let command = `open -g -a Preview.app "${filePath}"` 9 | if (!this.shouldOpenInBackground()) { 10 | command = command.replace(/-g\s/, '') 11 | } 12 | 13 | await latex.process.executeChildProcess(command, { showError: true }) 14 | } 15 | 16 | canOpen (filePath) { 17 | return process.platform === 'darwin' && (isPdfFile(filePath) || isPsFile(filePath)) 18 | } 19 | 20 | canOpenInBackground () { 21 | return true 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /lib/openers/qpdfview-opener.js: -------------------------------------------------------------------------------- 1 | /** @babel */ 2 | 3 | import fs from 'fs-plus' 4 | import Opener from '../opener' 5 | import { isPdfFile, isPsFile } from '../werkzeug' 6 | 7 | export default class QpdfviewOpener extends Opener { 8 | async open (filePath, texPath, lineNumber) { 9 | const qpdfviewPath = atom.config.get('latex.qpdfviewPath') 10 | const args = [ 11 | `--unique`, 12 | `"${filePath}"#src:"${texPath}":${lineNumber}:0` 13 | ] 14 | const command = `"${qpdfviewPath}" ${args.join(' ')}` 15 | await latex.process.executeChildProcess(command, { showError: true }) 16 | } 17 | 18 | canOpen (filePath) { 19 | return process.platform === 'linux' && 20 | (isPdfFile(filePath) || isPsFile(filePath)) && 21 | fs.existsSync(atom.config.get('latex.qpdfviewPath')) 22 | } 23 | 24 | hasSynctex () { 25 | return true 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /lib/openers/shell-open-opener.js: -------------------------------------------------------------------------------- 1 | /** @babel */ 2 | 3 | import Opener from '../opener' 4 | 5 | export default class ShellOpenOpener extends Opener { 6 | // shell open does not support texPath and lineNumber. 7 | async open (filePath, texPath, lineNumber) { 8 | const command = `"${filePath}"` 9 | 10 | await latex.process.executeChildProcess(command, { showError: true }) 11 | } 12 | 13 | canOpen (filePath) { 14 | return process.platform === 'win32' 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /lib/openers/skim-opener.js: -------------------------------------------------------------------------------- 1 | /** @babel */ 2 | 3 | import fs from 'fs-plus' 4 | import { heredoc } from '../werkzeug' 5 | import Opener from '../opener' 6 | 7 | export default class SkimOpener extends Opener { 8 | async open (filePath, texPath, lineNumber) { 9 | const skimPath = atom.config.get('latex.skimPath') 10 | const shouldActivate = !this.shouldOpenInBackground() 11 | const command = heredoc(` 12 | osascript -e \ 13 | " 14 | set theLine to \\"${lineNumber}\\" as integer 15 | set theFile to POSIX file \\"${filePath}\\" 16 | set theSource to POSIX file \\"${texPath}\\" 17 | set thePath to POSIX path of (theFile as alias) 18 | tell application \\"${skimPath}\\" 19 | if ${shouldActivate} then activate 20 | try 21 | set theDocs to get documents whose path is thePath 22 | if (count of theDocs) > 0 then revert theDocs 23 | end try 24 | open theFile 25 | tell front document to go to TeX line theLine from theSource 26 | end tell 27 | " 28 | `) 29 | 30 | await latex.process.executeChildProcess(command, { showError: true }) 31 | } 32 | 33 | canOpen (filePath) { 34 | return process.platform === 'darwin' && fs.existsSync(atom.config.get('latex.skimPath')) 35 | } 36 | 37 | hasSynctex () { 38 | return true 39 | } 40 | 41 | canOpenInBackground () { 42 | return true 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /lib/openers/sumatra-opener.js: -------------------------------------------------------------------------------- 1 | /** @babel */ 2 | 3 | import fs from 'fs-plus' 4 | import Opener from '../opener' 5 | import { isPdfFile } from '../werkzeug' 6 | 7 | export default class SumatraOpener extends Opener { 8 | async open (filePath, texPath, lineNumber) { 9 | const sumatraPath = `"${atom.config.get('latex.sumatraPath')}"` 10 | const atomPath = process.argv[0] 11 | const args = [ 12 | '-reuse-instance', 13 | '-forward-search', 14 | `"${texPath}"`, 15 | lineNumber, 16 | '-inverse-search', 17 | `"\\"${atomPath}\\" \\"%f:%l\\""`, 18 | `"${filePath}"` 19 | ] 20 | 21 | const command = `${sumatraPath} ${args.join(' ')}` 22 | 23 | await latex.process.executeChildProcess(command) 24 | } 25 | 26 | canOpen (filePath) { 27 | return process.platform === 'win32' && isPdfFile(filePath) && 28 | fs.existsSync(atom.config.get('latex.sumatraPath')) 29 | } 30 | 31 | hasSynctex () { 32 | return true 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /lib/openers/x-reader-opener.js: -------------------------------------------------------------------------------- 1 | /** @babel */ 2 | 3 | import EvinceOpener from './evince-opener' 4 | 5 | const DBUS_NAMES = { 6 | applicationObject: '/org/x/reader/Xreader', 7 | applicationInterface: 'org.x.reader.Application', 8 | 9 | daemonService: 'org.x.reader.Daemon', 10 | daemonObject: '/org/x/reader/Daemon', 11 | daemonInterface: 'org.x.reader.Daemon', 12 | 13 | windowInterface: 'org.x.reader.Window' 14 | } 15 | 16 | export default class XReaderOpener extends EvinceOpener { 17 | constructor () { 18 | super('Xreader', DBUS_NAMES) 19 | } 20 | 21 | canOpenInBackground () { 22 | return false 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /lib/openers/xdg-open-opener.js: -------------------------------------------------------------------------------- 1 | /** @babel */ 2 | 3 | import Opener from '../opener' 4 | 5 | export default class XdgOpenOpener extends Opener { 6 | // xdg-open does not support texPath and lineNumber. 7 | async open (filePath, texPath, lineNumber) { 8 | const command = `xdg-open "${filePath}"` 9 | 10 | await latex.process.executeChildProcess(command, { showError: true }) 11 | } 12 | 13 | canOpen (filePath) { 14 | return process.platform === 'linux' 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /lib/openers/zathura-opener.js: -------------------------------------------------------------------------------- 1 | /** @babel */ 2 | 3 | import fs from 'fs-plus' 4 | import Opener from '../opener' 5 | import { isPdfFile, isPsFile } from '../werkzeug' 6 | 7 | export default class ZathuraOpener extends Opener { 8 | async open (filePath, texPath, lineNumber) { 9 | const zathuraPath = atom.config.get('latex.zathuraPath') 10 | const atomPath = process.argv[0] 11 | const args = [ 12 | `--synctex-editor-command="\\"${atomPath}\\" \\"%{input}:%{line}\\""`, 13 | `--synctex-forward="${lineNumber}:1:${texPath}"`, 14 | `"${filePath}"` 15 | ] 16 | const command = `"${zathuraPath}" ${args.join(' ')}` 17 | await latex.process.executeChildProcess(command, { showError: true }) 18 | } 19 | 20 | canOpen (filePath) { 21 | return process.platform === 'linux' && 22 | (isPdfFile(filePath) || isPsFile(filePath)) && 23 | fs.existsSync(atom.config.get('latex.zathuraPath')) 24 | } 25 | 26 | hasSynctex () { 27 | return true 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /lib/parser.js: -------------------------------------------------------------------------------- 1 | /** @babel */ 2 | 3 | import fs from 'fs-plus' 4 | 5 | export default class Parser { 6 | constructor (filePath) { 7 | this.filePath = filePath 8 | } 9 | 10 | parse () {} 11 | 12 | getLines (defaultLines) { 13 | if (!fs.existsSync(this.filePath)) { 14 | if (defaultLines) return defaultLines 15 | throw new Error(`No such file: ${this.filePath}`) 16 | } 17 | 18 | const rawFile = fs.readFileSync(this.filePath, {encoding: 'utf-8'}) 19 | const lines = rawFile.replace(/(\r\n)|\r/g, '\n').split('\n') 20 | return lines 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /lib/parsers/fdb-parser.js: -------------------------------------------------------------------------------- 1 | /** @babel */ 2 | 3 | import Parser from '../parser.js' 4 | 5 | const SECTION_PATTERN = /^\["([^"]+)"]/ 6 | const GROUP_PATTERN = /^\s+\(([^)]+)\)/ 7 | const FILE_PATTERN = /^\s+"([^"]*)"/ 8 | 9 | export default class FdbParser extends Parser { 10 | parse () { 11 | let results = {} 12 | let section 13 | let group 14 | 15 | for (const line of this.getLines()) { 16 | const sectionMatch = line.match(SECTION_PATTERN) 17 | if (sectionMatch) { 18 | section = sectionMatch[1] 19 | results[section] = {} 20 | group = 'source' 21 | results[section][group] = [] 22 | continue 23 | } 24 | 25 | if (!section) continue 26 | 27 | const groupMatch = line.match(GROUP_PATTERN) 28 | if (groupMatch) { 29 | group = groupMatch[1] 30 | if (!results[section][group]) { 31 | results[section][group] = [] 32 | } 33 | continue 34 | } 35 | 36 | if (!group) continue 37 | 38 | const fileMatch = line.match(FILE_PATTERN) 39 | if (fileMatch) { 40 | results[section][group].push(fileMatch[1]) 41 | } 42 | } 43 | 44 | return results 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /lib/parsers/log-parser.js: -------------------------------------------------------------------------------- 1 | /** @babel */ 2 | 3 | import Parser from '../parser.js' 4 | import path from 'path' 5 | 6 | /* eslint-disable no-multi-spaces */ 7 | 8 | const OUTPUT_PATTERN = new RegExp('' + 9 | '^Output\\swritten\\son\\s' + // Leading text. 10 | '(.*)' + // Output path. 11 | '\\s\\(.*\\)\\.$' // Trailing text. 12 | ) 13 | 14 | // Error pattern 15 | const ERROR_PATTERN = new RegExp('' + 16 | '^(?:(.*):(\\d+):|!)' + // File path and line number 17 | '(?: (.+) Error:)? ' + // Error type 18 | '(.+?)\\.?$' // Message text, the ending period is optional for MiKTeX 19 | ) 20 | 21 | // Pattern for overfull/underfull boxes 22 | const BOX_PATTERN = new RegExp('' + 23 | '^((?:Over|Under)full \\\\[vh]box \\([^)]*\\))' + // Message text 24 | ' in paragraph at lines (\\d+)--(\\d+)$' // Line range 25 | ) 26 | 27 | // Warning and Info pattern 28 | const WARNING_INFO_PATTERN = new RegExp('' + 29 | '^((?:(?:Class|Package) \\S+)|LaTeX|LaTeX Font) ' + // Message origin 30 | '(Warning|Info):\\s+' + // Message type 31 | '(.*?)' + // Message text 32 | '(?: on input line (\\d+))?\\.$' // Line number 33 | ) 34 | 35 | /* eslint-enable no-multi-spaces */ 36 | 37 | // Pattern for font messages that overflow onto the next line. We do not capture 38 | // anything from the match, but we need to know where the error message is 39 | // located in the log file. 40 | const INCOMPLETE_FONT_PATTERN = /^LaTeX Font .*[^.]$/ 41 | 42 | // Pattern for \input markers which are surrounded by parentheses. 43 | const INPUT_FILE_PATTERN = /(\([^()[]+|\))/g 44 | 45 | // Pattern to remove leading and trailing spaces, quotes and left parenthesis. 46 | const INPUT_FILE_TRIM_PATTERN = /(^\([\s"]*|[\s"]+$)/g 47 | 48 | export default class LogParser extends Parser { 49 | constructor (filePath, texFilePath) { 50 | super(filePath) 51 | this.texFilePath = texFilePath 52 | this.projectPath = path.dirname(texFilePath) 53 | } 54 | 55 | parse () { 56 | const result = { 57 | logFilePath: this.filePath, 58 | outputFilePath: null, 59 | messages: [] 60 | } 61 | const sourcePaths = [this.texFilePath] 62 | 63 | const lines = this.getLines() 64 | lines.forEach((line, index) => { 65 | // Ignore the first line because it has some confusing patterns 66 | if (index === 0) return 67 | 68 | // Simplest Thing That Works™ and KISS® 69 | const logRange = [[index, 0], [index, line.length]] 70 | let match = line.match(OUTPUT_PATTERN) 71 | if (match) { 72 | const filePath = match[1].replace(/"/g, '') // TODO: Fix with improved regex. 73 | result.outputFilePath = path.resolve(this.projectPath, filePath) 74 | return 75 | } 76 | 77 | match = line.match(ERROR_PATTERN) 78 | if (match) { 79 | const lineNumber = match[2] ? parseInt(match[2], 10) : undefined 80 | result.messages.push({ 81 | type: 'error', 82 | text: (match[3] && match[3] !== 'LaTeX') ? match[3] + ': ' + match[4] : match[4], 83 | filePath: match[1] ? path.resolve(this.projectPath, match[1]) : sourcePaths[0], 84 | range: lineNumber ? [[lineNumber - 1, 0], [lineNumber - 1, Number.MAX_SAFE_INTEGER]] : undefined, 85 | logPath: this.filePath, 86 | logRange: logRange 87 | }) 88 | return 89 | } 90 | 91 | match = line.match(BOX_PATTERN) 92 | if (match) { 93 | result.messages.push({ 94 | type: 'warning', 95 | text: match[1], 96 | filePath: sourcePaths[0], 97 | range: [[parseInt(match[2], 10) - 1, 0], [parseInt(match[3], 10) - 1, Number.MAX_SAFE_INTEGER]], 98 | logPath: this.filePath, 99 | logRange: logRange 100 | }) 101 | return 102 | } 103 | 104 | match = (INCOMPLETE_FONT_PATTERN.test(line) ? line + lines[index + 1].substring(15) : line).match(WARNING_INFO_PATTERN) 105 | if (match) { 106 | const lineNumber = match[4] ? parseInt(match[4], 10) : undefined 107 | result.messages.push({ 108 | type: match[2].toLowerCase(), 109 | text: ((match[1] !== 'LaTeX') ? match[1] + ': ' + match[3] : match[3]).replace(/\s+/g, ' '), 110 | filePath: sourcePaths[0], 111 | range: lineNumber ? [[lineNumber - 1, 0], [lineNumber - 1, Number.MAX_SAFE_INTEGER]] : undefined, 112 | logPath: this.filePath, 113 | logRange: logRange 114 | }) 115 | } 116 | 117 | // Keep a stack of source paths indicated by input parentheses. We may 118 | // capture phrases that are enclosed in parathesis that are not paths, but 119 | // this should ignored safely since the closing paratheses will pop the 120 | // path right back off of the source path stack. 121 | match = line.match(INPUT_FILE_PATTERN) 122 | if (match) { 123 | for (const token of match) { 124 | if (token === ')') { 125 | // Avoid popping texFilePath off of the stack. 126 | if (sourcePaths.length > 1) sourcePaths.shift() 127 | } else { 128 | sourcePaths.unshift(path.resolve(this.projectPath, token.replace(INPUT_FILE_TRIM_PATTERN, ''))) 129 | } 130 | } 131 | } 132 | }) 133 | 134 | return result 135 | } 136 | } 137 | -------------------------------------------------------------------------------- /lib/parsers/magic-parser.js: -------------------------------------------------------------------------------- 1 | /** @babel */ 2 | 3 | import Parser from '../parser.js' 4 | 5 | /* eslint-disable no-multi-spaces */ 6 | 7 | const MAGIC_COMMENT_PATTERN = new RegExp('' + 8 | '^%\\s*' + // Optional whitespace. 9 | '!T[Ee]X' + // Magic marker. 10 | '\\s+' + // Semi-optional whitespace. 11 | '(\\w+)' + // [1] Captures the magic keyword. E.g. 'root'. 12 | '\\s*=\\s*' + // Equal sign wrapped in optional whitespace. 13 | '(.*)' + // [2] Captures everything following the equal sign. 14 | '$' // EOL. 15 | ) 16 | 17 | const LATEX_COMMAND_PATTERN = new RegExp('' + 18 | '\\' + // starting command \ 19 | '\\w+' + // command name e.g. input 20 | '(\\{|\\w|\\}|/|\\]|\\[)*' // options to the command 21 | ) 22 | 23 | /* eslint-enable no-multi-spaces */ 24 | 25 | export default class MagicParser extends Parser { 26 | parse () { 27 | const result = {} 28 | const lines = this.getLines([]) 29 | for (const line of lines) { 30 | const latexCommandMatch = line.match(LATEX_COMMAND_PATTERN) 31 | if (latexCommandMatch) { break } // Stop parsing if a latex command was found 32 | 33 | const match = line.match(MAGIC_COMMENT_PATTERN) 34 | if (match != null) { 35 | result[match[1]] = match[2].trim() 36 | } 37 | } 38 | 39 | return result 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /lib/process-manager.js: -------------------------------------------------------------------------------- 1 | /** @babel */ 2 | 3 | import childProcess from 'child_process' 4 | import kill from 'tree-kill' 5 | import { Disposable } from 'atom' 6 | 7 | export default class ProcessManager extends Disposable { 8 | processes = new Set() 9 | 10 | constructor () { 11 | super(() => this.killChildProcesses()) 12 | } 13 | 14 | executeChildProcess (command, options = {}) { 15 | const { allowKill, showError, ...execOptions } = options 16 | return new Promise(resolve => { 17 | // Windows does not like \$ appearing in command lines so only escape 18 | // if we need to. 19 | if (process.platform !== 'win32') command = command.replace('$', '\\$') 20 | const { pid } = childProcess.exec(command, execOptions, (error, stdout, stderr) => { 21 | if (allowKill) { 22 | this.processes.delete(pid) 23 | } 24 | if (error && showError && latex && latex.log) { 25 | latex.log.error(`An error occurred while trying to run "${command}" (${error.code}).`) 26 | } 27 | resolve({ 28 | statusCode: error ? error.code : 0, 29 | stdout, 30 | stderr 31 | }) 32 | }) 33 | if (allowKill) { 34 | this.processes.add(pid) 35 | } 36 | }) 37 | } 38 | 39 | killChildProcesses () { 40 | for (const pid of this.processes.values()) { 41 | kill(pid) 42 | } 43 | this.processes.clear() 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /lib/status-indicator.js: -------------------------------------------------------------------------------- 1 | /** @babel */ 2 | 3 | import StatusLabel from './views/status-label' 4 | import { Disposable } from 'atom' 5 | 6 | export default class StatusIndicator extends Disposable { 7 | constructor () { 8 | super(() => this.detachStatusBar()) 9 | } 10 | 11 | attachStatusBar (statusBar) { 12 | this.statusLabel = new StatusLabel() 13 | this.statusTile = statusBar.addLeftTile({ 14 | item: this.statusLabel, 15 | priority: 9001 16 | }) 17 | } 18 | 19 | detachStatusBar () { 20 | if (this.statusTile) { 21 | this.statusTile.destroy() 22 | this.statusTile = null 23 | } 24 | if (this.statusLabel) { 25 | this.statusLabel.destroy() 26 | this.statusLabel = null 27 | } 28 | } 29 | 30 | setBusy () { 31 | if (this.statusLabel) { 32 | this.statusLabel.update({ busy: true }) 33 | } 34 | } 35 | 36 | setIdle () { 37 | if (this.statusLabel) { 38 | this.statusLabel.update({ busy: false }) 39 | } 40 | } 41 | 42 | show (text, type, icon, spin, title, onClick) { 43 | if (this.statusLabel) { 44 | this.statusLabel.update({ text, type, icon, spin, title, onClick }) 45 | } 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /lib/views/file-reference.js: -------------------------------------------------------------------------------- 1 | /** @babel */ 2 | /** @jsx etch.dom */ 3 | 4 | import etch from 'etch' 5 | import path from 'path' 6 | 7 | export default class FileReference { 8 | constructor (properties = { type: 'error' }) { 9 | this.properties = properties 10 | etch.initialize(this) 11 | } 12 | 13 | async destroy () { 14 | await etch.destroy(this) 15 | } 16 | 17 | render () { 18 | const { file, range } = this.properties 19 | 20 | if (!file) return 21 | 22 | const endLineReference = (range && range[0][0] !== range[1][0]) ? `\u2013${range[1][0] + 1}` : '' 23 | const lineReference = range ? ` (${range[0][0] + 1}${endLineReference})` : '' 24 | const text = path.basename(file) 25 | const clickHandler = () => { 26 | atom.workspace.open(file, { initialLine: range ? range[0][0] : 0 }) 27 | } 28 | 29 | return ( 30 | 31 | {text} 32 | {lineReference} 33 | 34 | ) 35 | } 36 | 37 | update (properties) { 38 | this.properties = properties 39 | return etch.update(this) 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /lib/views/log-dock.js: -------------------------------------------------------------------------------- 1 | /** @babel */ 2 | /** @jsx etch.dom */ 3 | 4 | import etch from 'etch' 5 | import { CompositeDisposable } from 'atom' 6 | import LogMessage from './log-message' 7 | 8 | export default class LogDock { 9 | static LOG_DOCK_URI = 'atom://latex/log' 10 | 11 | disposables = new CompositeDisposable() 12 | 13 | constructor (properties = {}) { 14 | this.properties = properties 15 | etch.initialize(this) 16 | this.disposables.add(latex.log.onMessages(() => this.update())) 17 | } 18 | 19 | async destroy () { 20 | this.disposables.dispose() 21 | await etch.destroy(this) 22 | } 23 | 24 | render () { 25 | let content = latex.log.getMessages().map(message => ) 26 | 27 | return ( 28 |
29 |
30 | 31 | 32 | 33 | 35 | 36 | 37 | 38 | 39 | {content} 40 |
34 | MessageSource FileLog File
41 |
42 |
43 | ) 44 | } 45 | 46 | update (properties = {}) { 47 | this.properties = properties 48 | return etch.update(this) 49 | } 50 | 51 | readAfterUpdate () { 52 | // Look for highlighted messages and scroll to them 53 | const highlighted = this.refs.body.getElementsByClassName('latex-highlight') 54 | if (highlighted.length) { 55 | highlighted[0].scrollIntoView() 56 | } 57 | } 58 | 59 | getTitle () { 60 | return 'LaTeX Log' 61 | } 62 | 63 | getURI () { 64 | return LogDock.LOG_DOCK_URI 65 | } 66 | 67 | getDefaultLocation () { 68 | return 'bottom' 69 | } 70 | 71 | serialize () { 72 | return { 73 | deserializer: 'latex/log' 74 | } 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /lib/views/log-message.js: -------------------------------------------------------------------------------- 1 | /** @babel */ 2 | /** @jsx etch.dom */ 3 | 4 | import etch from 'etch' 5 | import { Range } from 'atom' 6 | import MessageIcon from './message-icon' 7 | import FileReference from './file-reference' 8 | 9 | export default class LogMessage { 10 | constructor (properties = {}) { 11 | this.properties = properties 12 | etch.initialize(this) 13 | } 14 | 15 | async destroy () { 16 | await etch.destroy(this) 17 | } 18 | 19 | render () { 20 | const message = this.properties.message 21 | const lines = message.text.split('\n').map(line => (
{line}
)) 22 | 23 | return ( 24 | 25 | 26 | {lines} 27 | 28 | 29 | 30 | ) 31 | } 32 | 33 | getClassNames (message) { 34 | const className = `latex-${message.type}` 35 | 36 | const matchesFilePath = message.filePath && this.properties.filePath === message.filePath 37 | const containsPosition = message.range && this.properties.position && Range.fromObject(message.range).containsPoint(this.properties.position) 38 | if (matchesFilePath && containsPosition) { 39 | return `${className} latex-highlight` 40 | } 41 | 42 | return className 43 | } 44 | 45 | update (properties) { 46 | this.properties = properties 47 | return etch.update(this) 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /lib/views/message-count.js: -------------------------------------------------------------------------------- 1 | /** @babel */ 2 | /** @jsx etch.dom */ 3 | 4 | import etch from 'etch' 5 | import { CompositeDisposable } from 'atom' 6 | import MessageIcon from './message-icon' 7 | 8 | export default class MessageCount { 9 | disposables = new CompositeDisposable() 10 | 11 | constructor (properties = { type: 'error' }) { 12 | this.properties = properties 13 | etch.initialize(this) 14 | this.disposables.add(latex.log.onMessages(() => this.update())) 15 | } 16 | 17 | async destroy () { 18 | await etch.destroy(this) 19 | this.disposables.dispose() 20 | } 21 | 22 | render () { 23 | if (latex.log.messageTypeIsVisible(this.properties.type)) { 24 | const counts = latex.log.getMessages().reduce((total, message) => message.type === this.properties.type ? total + 1 : total, 0) 25 | 26 | return ( 27 | 28 | 29 | {counts} 30 | 31 | ) 32 | } 33 | 34 | return 35 | } 36 | 37 | update (properties = {}) { 38 | Object.assign(this.properties, properties) 39 | return etch.update(this) 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /lib/views/message-icon.js: -------------------------------------------------------------------------------- 1 | /** @babel */ 2 | /** @jsx etch.dom */ 3 | 4 | import etch from 'etch' 5 | 6 | export default class MessageIcon { 7 | static icons = { 8 | error: 'stop', 9 | warning: 'alert', 10 | info: 'info' 11 | } 12 | 13 | constructor (properties = { type: 'error' }) { 14 | this.properties = properties 15 | etch.initialize(this) 16 | } 17 | 18 | async destroy () { 19 | await etch.destroy(this) 20 | } 21 | 22 | render () { 23 | return ( 24 | 25 | ) 26 | } 27 | 28 | update (properties) { 29 | this.properties = properties 30 | return etch.update(this) 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /lib/views/status-label.js: -------------------------------------------------------------------------------- 1 | /** @babel */ 2 | /** @jsx etch.dom */ 3 | 4 | import etch from 'etch' 5 | import MessageCount from './message-count' 6 | 7 | export default class StatusLabel { 8 | constructor (properties = {}) { 9 | this.properties = properties 10 | etch.initialize(this) 11 | } 12 | 13 | async destroy () { 14 | if (this.tooltip) { 15 | this.tooltip.dispose() 16 | } 17 | await etch.destroy(this) 18 | } 19 | 20 | render () { 21 | return ( 22 |
latex.log.show()}> 23 | 24 | LaTeX 25 | 26 | 27 | 28 |
29 | ) 30 | } 31 | 32 | getClassNames () { 33 | const className = `latex-status inline-block` 34 | 35 | if (this.properties.busy) { 36 | return `${className} is-busy` 37 | } 38 | 39 | return className 40 | } 41 | 42 | update (properties = {}) { 43 | Object.assign(this.properties, properties) 44 | return etch.update(this) 45 | } 46 | 47 | readAfterUpdate () { 48 | if (this.tooltip) { 49 | this.tooltip.dispose() 50 | this.tooltip = null 51 | } 52 | this.tooltip = atom.tooltips.add(this.element, { title: 'Click to show LaTeX log' }) 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /lib/werkzeug.js: -------------------------------------------------------------------------------- 1 | /** @babel */ 2 | 3 | import _ from 'lodash' 4 | import url from 'url' 5 | 6 | export default { 7 | pathToUri (filePath, hash) { 8 | if (process.platform === 'win32') { 9 | filePath = filePath.replace(/\\/g, '/') 10 | if (!filePath.startsWith('/')) { 11 | filePath = `/${filePath}` 12 | } 13 | } 14 | 15 | const urlObject = { 16 | protocol: 'file:', 17 | slashes: true, 18 | pathname: encodeURI(filePath).replace(/[?#]/g, encodeURIComponent) 19 | } 20 | 21 | if (hash) urlObject.hash = encodeURIComponent(hash) 22 | 23 | return url.format(urlObject) 24 | }, 25 | 26 | uriToPath (uri) { 27 | let filePath = decodeURI(url.parse(uri).pathname || '') 28 | 29 | if (process.platform === 'win32') { 30 | filePath = filePath.replace(/\//g, '\\').replace(/^(.+)\|/, '$1:').replace(/\\([A-Z]:\\)/, '$1') 31 | } else if (!filePath.startsWith('/')) { 32 | filePath = `/${filePath}` 33 | } 34 | 35 | return filePath 36 | }, 37 | 38 | heredoc (input) { 39 | if (input === null) { return null } 40 | 41 | const lines = _.dropWhile(input.split(/\r\n|\n|\r/), line => line.length === 0) 42 | const indentLength = _.takeWhile(lines[0], char => char === ' ').length 43 | const truncatedLines = lines.map(line => line.slice(indentLength)) 44 | 45 | return truncatedLines.join('\n') 46 | }, 47 | 48 | promisify (target) { 49 | return (...args) => { 50 | return new Promise((resolve, reject) => { 51 | target(...args, (error, data) => { error ? reject(error) : resolve(data) }) 52 | }) 53 | } 54 | }, 55 | 56 | getEditorDetails () { 57 | const editor = atom.workspace.getActiveTextEditor() 58 | if (!editor) return {} 59 | 60 | const filePath = editor.getPath() 61 | const position = editor.getCursorBufferPosition() 62 | const lineNumber = position.row + 1 63 | 64 | return { editor, filePath, position, lineNumber } 65 | }, 66 | 67 | replacePropertiesInString (text, properties) { 68 | return _.reduce(properties, (current, value, name) => current.replace(`{${name}}`, value), text) 69 | }, 70 | 71 | isSourceFile (filePath) { 72 | return filePath && !!filePath.match(/\.(?:tex|tikz|lhs|lagda|[prs]nw)$/i) 73 | }, 74 | 75 | isTexFile (filePath) { 76 | return filePath && !!filePath.match(/\.(?:tex|lhs|lagda)$/i) 77 | }, 78 | 79 | isKnitrFile (filePath) { 80 | return filePath && !!filePath.match(/\.[rs]nw$/i) 81 | }, 82 | 83 | isPdfFile (filePath) { 84 | return filePath && !!filePath.match(/\.pdf$/i) 85 | }, 86 | 87 | isPsFile (filePath) { 88 | return filePath && !!filePath.match(/\.ps$/i) 89 | }, 90 | 91 | isDviFile (filePath) { 92 | return filePath && !!filePath.match(/\.(?:dvi|xdv)$/i) 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /menus/latex.json: -------------------------------------------------------------------------------- 1 | { 2 | "menu": [ 3 | { 4 | "label": "Packages", 5 | "submenu": [ 6 | { 7 | "label": "LaTeX", 8 | "submenu": [ 9 | { "label": "Build", "command": "latex:build" }, 10 | { "label": "Rebuild", "command": "latex:rebuild" }, 11 | { "label": "Clean", "command": "latex:clean" }, 12 | { "label": "Kill Build", "command": "latex:kill" }, 13 | { "type": "separator" }, 14 | { "label": "Toggle Log", "command": "latex:toggle-log" }, 15 | { "label": "Show Log", "command": "latex:show-log" }, 16 | { "label": "Hide Log", "command": "latex:hide-log" }, 17 | { "label": "Clear Log", "command": "latex:clear-log" }, 18 | { "type": "separator" }, 19 | { "label": "Check Runtime", "command": "latex:check-runtime" } 20 | ] 21 | } 22 | ] 23 | } 24 | ], 25 | "context-menu": { 26 | "atom-text-editor[data-grammar~=\"latex\"]": [ 27 | { "label": "LaTeX Sync", "command": "latex:sync" }, 28 | { "label": "LaTeX Sync Log", "command": "latex:sync-log" } 29 | ] 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "latex", 3 | "version": "0.50.2", 4 | "description": "Compile LaTeX documents from within Atom", 5 | "keywords": [ 6 | "tex", 7 | "latex", 8 | "latexmk", 9 | "texlive", 10 | "miktex" 11 | ], 12 | "license": "MIT", 13 | "repository": "https://github.com/thomasjo/atom-latex", 14 | "author": "Thomas Johansen ", 15 | "contributors": [ 16 | "Tarn Burton " 17 | ], 18 | "main": "./lib/main", 19 | "dependencies": { 20 | "@dicy/client": "^0.13.0", 21 | "dbus-native": "^0.4.0", 22 | "etch": "0.12.6", 23 | "fs-plus": "^3.0.0", 24 | "glob": "^7.1.1", 25 | "js-yaml": "^3.11.0", 26 | "lodash": "^4.17.4", 27 | "minimatch": "^3.0.4", 28 | "temp": "^0.8.3", 29 | "tree-kill": "^1.1.0", 30 | "wrench": "^1.5.9" 31 | }, 32 | "devDependencies": { 33 | "babel-eslint": "^8.2.2", 34 | "snazzy": "^7.1.1", 35 | "standard": "^11.0.0" 36 | }, 37 | "engines": { 38 | "atom": ">=1.19.0 <2.0.0" 39 | }, 40 | "standard": { 41 | "parser": "babel-eslint", 42 | "globals": [ 43 | "atom", 44 | "latex", 45 | "afterEach", 46 | "beforeEach", 47 | "describe", 48 | "expect", 49 | "it", 50 | "jasmine", 51 | "runs", 52 | "spyOn", 53 | "waitsFor", 54 | "waitsForPromise" 55 | ] 56 | }, 57 | "activationCommands": { 58 | "atom-workspace": [ 59 | "latex:build", 60 | "latex:check-runtime", 61 | "latex:clean", 62 | "latex:clear-log", 63 | "latex:hide-log", 64 | "latex:rebuild", 65 | "latex:show-log", 66 | "latex:sync-log", 67 | "latex:sync", 68 | "latex:toggle-log" 69 | ] 70 | }, 71 | "activationHooks": [ 72 | "language-latex:grammar-used", 73 | "language-tex:grammar-used", 74 | "language-latexsimple:grammar-used" 75 | ], 76 | "consumedServices": { 77 | "status-bar": { 78 | "versions": { 79 | "^1.0.0": "consumeStatusBar" 80 | } 81 | } 82 | }, 83 | "deserializers": { 84 | "latex/log": "deserializeLog" 85 | }, 86 | "configSchema": { 87 | "texPath": { 88 | "title": "TeX Path", 89 | "description": "The full path to your TeX distribution's bin directory. Supports `$PATH` substitution.", 90 | "type": "string", 91 | "default": "", 92 | "order": 1 93 | }, 94 | "engine": { 95 | "description": "Select standard LaTeX engine", 96 | "type": "string", 97 | "enum": [ 98 | "pdflatex", 99 | "lualatex", 100 | "platex", 101 | "uplatex", 102 | "xelatex" 103 | ], 104 | "default": "pdflatex", 105 | "order": 2 106 | }, 107 | "customEngine": { 108 | "description": "Enter command for custom LaTeX engine. Overrides Engine.", 109 | "type": "string", 110 | "default": "", 111 | "order": 3 112 | }, 113 | "enableShellEscape": { 114 | "type": "boolean", 115 | "default": false, 116 | "order": 4 117 | }, 118 | "enableSynctex": { 119 | "title": "Enable SyncTeX", 120 | "type": "boolean", 121 | "default": true, 122 | "order": 5 123 | }, 124 | "useDicy": { 125 | "title": "Use DiCy", 126 | "description": "Use the experimental javascript based builder [`DiCy`](https://yitzchak.github.io/dicy/) instead of `latexmk`. [`DiCy`](https://yitzchak.github.io/dicy/) is included with this package so no further configuration or installation is required.", 127 | "type": "boolean", 128 | "default": "false", 129 | "order": 6 130 | }, 131 | "enableExtendedBuildMode": { 132 | "description": "Enable extended build mode using `latexmk` rules for custom files types. Currently includes support for Asymptote, the `glossaries` package, the `index` package, MetaPost, the `nomencl` package and SageTeX. Please note that these rules are loaded after all other `latexmkrc` files are loaded, and therefore may overwrite custom rules defined by the user.", 133 | "type": "boolean", 134 | "default": true, 135 | "order": 7 136 | }, 137 | "loggingLevel": { 138 | "description": "The minimum level of message severity to output in the logger. A logging level of `error` shows only messages indicating catastrophic issues such as undefined symbols, `warning` shows error messages and messages indicating unintended consequences such as bad boxes, and `info` shows all messages including purely informational messages such a font loading.", 139 | "type": "string", 140 | "enum": [ 141 | "error", 142 | "warning", 143 | "info" 144 | ], 145 | "default": "warning", 146 | "order": 8 147 | }, 148 | "cleanPatterns": { 149 | "description": "The files and directories to remove during a LaTeX clean. Basic glob patterns are understood and named properties such as {jobname} are replaced with the current build properties. Patterns that start with `/` or `\\` are matched against any file in the same directory as the source file. All other patterns are matched against generated files in the output directory. More information can be found on the Atom LaTeX wiki.", 150 | "type": "array", 151 | "items": { 152 | "type": "string" 153 | }, 154 | "default": [ 155 | "**/*.aux", 156 | "**/*.aux.bak", 157 | "**/*.bbl", 158 | "**/*.bcf", 159 | "**/*.blg", 160 | "**/*.dvi", 161 | "**/*.fdb_latexmk", 162 | "**/*.fls", 163 | "**/*.idx", 164 | "**/*.idx.bak", 165 | "**/*.ilg", 166 | "**/*.ind", 167 | "**/*.lof", 168 | "**/*.log", 169 | "**/*.lol", 170 | "**/*.lot", 171 | "**/*.nav", 172 | "**/*.out", 173 | "**/*.pdf", 174 | "**/*.ps", 175 | "**/*.snm", 176 | "**/*.synctex.gz", 177 | "**/*.toc", 178 | "/**/_minted-{jobname}", 179 | "/{output_dir}/sage-plots-for-{jobname}.tex", 180 | "/missfont.log", 181 | "/texput.log", 182 | "/texput.aux" 183 | ], 184 | "order": 9 185 | }, 186 | "outputDirectory": { 187 | "description": "All files generated during a build will be redirected here. Leave blank if you want the build output to be stored in the same directory as the TeX document.", 188 | "type": "string", 189 | "default": "", 190 | "order": 10 191 | }, 192 | "outputFormat": { 193 | "description": "Output file format. DVI and PS currently only supported for latexmk.", 194 | "type": "string", 195 | "enum": [ 196 | "pdf", 197 | "dvi", 198 | "ps" 199 | ], 200 | "default": "pdf", 201 | "order": 11 202 | }, 203 | "producer": { 204 | "title": "PDF Producer", 205 | "description": "Program to use when post-processing DVI output in order to produce PDF. This is only used for LaTeX engines not capable of natively producing PDF. Currently only supported for latexmk.", 206 | "type": "string", 207 | "enum": [ 208 | "dvipdfmx", 209 | "xdvipdfmx", 210 | "dvipdf", 211 | "ps2pdf" 212 | ], 213 | "default": "dvipdfmx", 214 | "order": 12 215 | }, 216 | "moveResultToSourceDirectory": { 217 | "title": "Move Result to Source Directory", 218 | "description": "Ensures that the output file produced by a successful build is stored together with the TeX document that produced it.", 219 | "type": "boolean", 220 | "default": true, 221 | "order": 13 222 | }, 223 | "buildOnSave": { 224 | "title": "Build on Save", 225 | "description": "Automatically run builds when files are saved.", 226 | "type": "boolean", 227 | "default": false, 228 | "order": 14 229 | }, 230 | "openResultAfterBuild": { 231 | "title": "Open Result after Successful Build", 232 | "type": "boolean", 233 | "default": true, 234 | "order": 15 235 | }, 236 | "openResultInBackground": { 237 | "title": "Open Result in Background", 238 | "type": "boolean", 239 | "default": true, 240 | "order": 16 241 | }, 242 | "opener": { 243 | "type": "string", 244 | "enum": [ 245 | "automatic", 246 | "atril", 247 | "evince", 248 | "okular", 249 | "pdf-view", 250 | "preview", 251 | "qpdfview", 252 | "shell-open", 253 | "skim", 254 | "sumatra", 255 | "xdg-open", 256 | "x-reader", 257 | "zathura", 258 | "custom" 259 | ], 260 | "default": "automatic", 261 | "order": 17 262 | }, 263 | "pdfViewSplitDirection": { 264 | "description": "Pane split direction to use for pdf-view.", 265 | "type": "string", 266 | "enum": [ 267 | "left", 268 | "right", 269 | "up", 270 | "down" 271 | ], 272 | "default": "right", 273 | "order": 18 274 | }, 275 | "skimPath": { 276 | "description": "Full application path to Skim (macOS).", 277 | "type": "string", 278 | "default": "/Applications/Skim.app", 279 | "order": 19 280 | }, 281 | "sumatraPath": { 282 | "title": "SumatraPDF Path", 283 | "description": "Full application path to SumatraPDF (Windows).", 284 | "type": "string", 285 | "default": "C:\\Program Files (x86)\\SumatraPDF\\SumatraPDF.exe", 286 | "order": 20 287 | }, 288 | "okularPath": { 289 | "description": "Full application path to Okular (*nix).", 290 | "type": "string", 291 | "default": "/usr/bin/okular", 292 | "order": 21 293 | }, 294 | "zathuraPath": { 295 | "description": "Full application path to Zathura (*nix).", 296 | "type": "string", 297 | "default": "/usr/bin/zathura", 298 | "order": 22 299 | }, 300 | "qpdfviewPath": { 301 | "description": "Full application path to qpdfview (*nix).", 302 | "type": "string", 303 | "default": "/usr/bin/qpdfview", 304 | "order": 23 305 | }, 306 | "viewerPath": { 307 | "title": "Custom PDF Viewer Path", 308 | "description": "Full application path to your PDF viewer. Overrides Skim and SumatraPDF options.", 309 | "type": "string", 310 | "default": "", 311 | "order": 24 312 | } 313 | } 314 | } 315 | -------------------------------------------------------------------------------- /resources/latexmkrc: -------------------------------------------------------------------------------- 1 | ######################### 2 | # Support for Asymptote # 3 | ######################### 4 | 5 | add_cus_dep('asy', 'eps', 0, 'asymptote_eps'); 6 | add_cus_dep('asy', 'pdf', 0, 'asymptote_pdf'); 7 | add_cus_dep('asy', 'tex', 0, 'asymptote_tex'); 8 | 9 | sub asymptote_eps { 10 | return asymptote($_[0], 'eps'); 11 | } 12 | 13 | sub asymptote_pdf { 14 | return asymptote($_[0], 'pdf'); 15 | } 16 | 17 | sub asymptote_tex { 18 | return asymptote($_[0], 'tex'); 19 | } 20 | 21 | sub asymptote { 22 | $dir = dirname($_[0]); 23 | $file = basename($_[0]); 24 | my $ret = system("asy -offscreen -vv -f \"$_[1]\" -cd \"$dir\" \"$file\" >\"$_[0].asy_log\" 2>&1"); 25 | my $log_handle = new FileHandle; 26 | open $log_handle, "$_[0].asy_log"; 27 | %imp = (); 28 | 29 | while (<$log_handle>) { 30 | if (/^(Including|Loading) .* from (.*)\s*$/) { 31 | my $import = $2; 32 | $imp{$import} = 1; 33 | } elsif (/^(error|.*\.asy: \d)/) { 34 | warn "==Message from asy: $_"; 35 | $ret = 1; 36 | } elsif (/^(kpsewhich|Processing|Using|Welcome|Wrote|cd|gs) /) { 37 | } else { 38 | warn "==Message from asy: $_"; 39 | } 40 | } 41 | close $log_handle; 42 | if ($version_num >= '4.48') { 43 | rdb_set_source( $rule, keys %imp ); 44 | } 45 | return $ret; 46 | } 47 | 48 | push @generated_exts, 'asy_log'; 49 | 50 | ###################################### 51 | # Support for the glossaries package # 52 | ###################################### 53 | 54 | add_cus_dep('glo', 'gls', 0, 'makeglossaries'); 55 | add_cus_dep('acn', 'acr', 0, 'makeglossaries'); 56 | 57 | sub makeglossaries { 58 | $dir = dirname($_[0]); 59 | $file = basename($_[0]); 60 | system("makeglossaries -d \"$dir\" \"$file\""); 61 | } 62 | 63 | push @generated_exts, 'glo', 'gls', 'glg', 'acn', 'acr', 'alg'; 64 | 65 | ################################# 66 | # Support for the index package # 67 | ################################# 68 | 69 | add_cus_dep('adx', 'and', 0, 'acro_index'); 70 | 71 | sub acro_index { 72 | system( "makeindex -o \"$_[0].and\" \"$_[0].adx\""); 73 | } 74 | 75 | push @generated_exts, 'adx', 'and', 'alg'; 76 | 77 | add_cus_dep('bdx', 'bnd', 0, 'bib_index'); 78 | 79 | sub bib_index { 80 | system("makeindex -s bibref.ist -o \"$_[0].bnd\" \"$_[0].bdx\""); 81 | } 82 | 83 | push @generated_exts, 'bdx', 'bnd', 'blg'; 84 | 85 | add_cus_dep('ndx', 'nnd', 0, 'name_index'); 86 | 87 | sub name_index { 88 | system("makeindex -o \"$_[0].nnd\" \"$_[0].ndx\""); 89 | } 90 | 91 | push @generated_exts, 'ndx', 'nnd', 'nlg'; 92 | 93 | add_cus_dep('ldx', 'lnd', 0, 'list_index'); 94 | 95 | sub list_index { 96 | system("makeindex -o \"$_[0].lnd\" \"$_[0].ldx\""); 97 | } 98 | 99 | push @generated_exts, 'ldx', 'lnd', 'llg'; 100 | 101 | add_cus_dep('tdx', 'tnd', 0, 'title_index'); 102 | 103 | sub title_index { 104 | system("makeindex -o \"$_[0].tnd\" \"$_[0].tdx\""); 105 | } 106 | 107 | push @generated_exts, 'tdx', 'tnd', 'tlg'; 108 | 109 | ##################################### 110 | # Metapost support for feynmp, etc. # 111 | ##################################### 112 | 113 | add_cus_dep('mp', '1', 0, 'mpost'); 114 | 115 | sub mpost { 116 | $dir = dirname($_[0]); 117 | $file = basename($_[0]); 118 | return system("cd \"$dir\" && mpost \"$file\""); 119 | } 120 | 121 | push @generated_exts, '1'; 122 | 123 | ######################################## 124 | # Support for the nomenclature package # 125 | ######################################## 126 | 127 | add_cus_dep("nlo", "nls", 0, "nomencl_index"); 128 | 129 | sub nomencl_index { 130 | system("makeindex \"$_[0].nlo\" -s nomencl.ist -o \"$_[0].nls\" -t \"$_[0].nlg\""); 131 | } 132 | 133 | push @generated_exts, 'nlo', 'nls', 'nlg'; 134 | 135 | ####################### 136 | # Support for SageTeX # 137 | ####################### 138 | 139 | add_cus_dep('sage', 'sout', 0, 'sage'); 140 | 141 | $hash_calc_ignore_pattern{'sage'} = '^( _st_.goboom|print .SageT)'; 142 | 143 | sub sage { 144 | $dir = dirname($_[0]); 145 | $file = basename($_[0]); 146 | system("cd \"$dir\" && sage \"$file.sage\""); 147 | } 148 | 149 | push @generated_exts, 'sage', 'sout', 'sage.py', 'scmd'; 150 | -------------------------------------------------------------------------------- /script/cibuild: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | # Exit on failure, and treat expansion of unset variables as an error. 4 | set -eu 5 | 6 | # Enable case-insensitive pattern matching. 7 | shopt -s nocasematch 8 | 9 | ensure_xz_installed() { 10 | if [[ "${APPVEYOR:-}" == true ]]; then 11 | echo "Installing XZ..." 12 | local archive_path="/tmp/xz.7z" 13 | curl -Lso ${archive_path} "https://tukaani.org/xz/xz-5.2.3-windows.7z" 14 | 15 | local xz_root="${HOME}/xz" 16 | 7z x -o"${xz_root}" "${archive_path}" > /dev/null 17 | 18 | export PATH="${xz_root}/bin_x86-64:${PATH}" 19 | fi 20 | } 21 | 22 | initialize() { 23 | local script_dir="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd -P )" 24 | local scripts=( 25 | install-ghostscript 26 | install-knitr 27 | install-miktex 28 | install-texlive 29 | ) 30 | 31 | for script in ${scripts[@]}; do 32 | source "${script_dir}/${script}" 33 | done 34 | } 35 | 36 | install_latex_distribution() { 37 | if [[ "${TEX_DIST:=texlive}" == "miktex" ]]; then 38 | install_miktex 39 | else 40 | install_texlive 41 | fi 42 | } 43 | 44 | exec_ci() { 45 | if [[ "${APPVEYOR:-}" == true ]]; then 46 | local script_path="/tmp/build-package.ps1" 47 | curl -so ${script_path} "https://raw.githubusercontent.com/atom/ci/master/build-package.ps1" 48 | powershell -noninteractive -noprofile -command "${script_path}" 49 | else 50 | curl -s "https://raw.githubusercontent.com/atom/ci/master/build-package.sh" | sh 51 | fi 52 | } 53 | 54 | main() { 55 | initialize 56 | ensure_xz_installed 57 | install_latex_distribution 58 | install_knitr 59 | install_ghostscript 60 | exec_ci 61 | } 62 | 63 | main "$@" 64 | -------------------------------------------------------------------------------- /script/create-fixtures: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | shopt -s extglob 4 | 5 | cd spec/fixtures 6 | 7 | for i in pdf pdfps pdfdvi ps dvi; do 8 | latexmk -$i -outdir=log-parse -jobname=file-$i file.tex 9 | sed -i '/^ "\//d' log-parse/file-$i.fdb_latexmk 10 | done 11 | 12 | rm log-parse/*.!(log|fdb_latexmk) 13 | -------------------------------------------------------------------------------- /script/install-ghostscript: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | # Exit on failure, and treat expansion of unset variables as an error. 4 | set -eu 5 | 6 | # Enable case-insensitive pattern matching. 7 | shopt -s nocasematch 8 | 9 | install_ghostscript() { 10 | if [[ "${APPVEYOR:-}" == true ]]; then 11 | echo "Installing ghostscript..." 12 | choco install ghostscript 13 | elif [[ "${TRAVIS_OS_NAME:-}" == "osx" ]]; then 14 | echo "Installing ghostscript..." 15 | brew update 16 | brew install ghostscript 17 | fi 18 | } 19 | -------------------------------------------------------------------------------- /script/install-knitr: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | # Exit on failure, and treat expansion of unset variables as an error. 4 | set -eu 5 | 6 | # Enable case-insensitive pattern matching. 7 | shopt -s nocasematch 8 | 9 | ensure_r_installed() { 10 | if [[ "${APPVEYOR:-}" == true ]]; then 11 | # TODO: Remove when 'R.Project' package adds it as a dependency. 12 | echo "Installing chocolatey-core.extension..." 13 | choco install chocolatey-core.extension 14 | 15 | echo "Installing R..." 16 | choco install R.Project --allow-empty-checksums --ia="/DIR=\"${R_HOME}\"" 17 | 18 | export PATH="${R_HOME}/bin:${PATH}" 19 | elif [[ "${TRAVIS_OS_NAME:-}" == "osx" ]]; then 20 | echo "Installing R..." 21 | brew update 22 | brew install r 23 | fi 24 | } 25 | 26 | install_knitr() { 27 | ensure_r_installed 28 | 29 | # Check if we need to install Knitr. 30 | if ! Rscript -e "library(knitr); library(patchSynctex)" &> /dev/null; then 31 | # Make sure the R_LIBS_USER directory exists to make R happy. 32 | [[ ! -d "${R_LIBS_USER}" ]] && mkdir -p "${R_LIBS_USER}" 33 | 34 | echo "Installing knitr..." 35 | Rscript -e "install.packages(c('knitr', 'patchSynctex'), repos = 'http://cran.r-project.org')" 36 | else 37 | echo "Using cached installation of knitr" 38 | fi 39 | } 40 | -------------------------------------------------------------------------------- /script/install-miktex: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | # Exit on failure, and treat expansion of unset variables as an error. 4 | set -eu 5 | 6 | # Enable case-insensitive pattern matching. 7 | shopt -s nocasematch 8 | 9 | install_miktex() { 10 | echo "Installing MiKTeX..." 11 | curl -sL "https://www.dropbox.com/s/yop91s5bcq4moz5/miktex-portable.tar.xz?dl=1" | tar -xJC "${HOME}" 12 | 13 | # Ensure PATH points to the binaries. 14 | export PATH="${HOME}/miktex-portable/texmfs/install/miktex/bin:${HOME}/miktex-portable/asymptote:${PATH}" 15 | export ASYMPTOTE_HOME="${HOME}/miktex-portable/asymptote" 16 | } 17 | -------------------------------------------------------------------------------- /script/install-texlive: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | # Exit on failure, and treat expansion of unset variables as an error. 4 | set -eu 5 | 6 | # Enable case-insensitive pattern matching. 7 | shopt -s nocasematch 8 | 9 | get_download_url() { 10 | local platform="travis" 11 | [[ "${APPVEYOR:-}" == true ]] && platform="appveyor" 12 | echo "https://raw.githubusercontent.com/thomasjo/${platform}-texlive/master/texlive.tar.xz" 13 | } 14 | 15 | install_texlive() { 16 | echo "Downloading portable TeX Live installation..." 17 | curl -s $( get_download_url ) | tar -xJC "${HOME}" 18 | 19 | # Ensure PATH points to the platform-specific binaries. 20 | if [[ "${APPVEYOR:-}" == true ]]; then 21 | export PATH="${HOME}/texlive/bin/win32:${PATH}" 22 | elif [[ "${TRAVIS_OS_NAME:-}" == "linux" ]]; then 23 | export PATH="${HOME}/texlive/bin/x86_64-linux:${PATH}" 24 | else 25 | export PATH="${HOME}/texlive/bin/x86_64-darwin:${PATH}" 26 | fi 27 | } 28 | -------------------------------------------------------------------------------- /script/lint: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | # Exit on failure, and treat expansion of unset variables as an error. 4 | set -eu 5 | 6 | lint() { 7 | local npm_bin=$( npm bin ) 8 | local paths="$1" 9 | 10 | "${npm_bin}/standard" --verbose "${paths}" | "${npm_bin}/snazzy" 11 | } 12 | 13 | main() { 14 | local script_dir="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd -P )" 15 | local source_dir=$( dirname "${script_dir}" ) 16 | 17 | local -i status=0 18 | echo "Linting package..." && lint "${source_dir}/lib/**/*.js" || status=$? 19 | echo "Linting package specs..." && lint "${source_dir}/spec/**/*.js" || status=$? 20 | return ${status} 21 | } 22 | 23 | main "$@" 24 | -------------------------------------------------------------------------------- /spec/async-spec-helpers.js: -------------------------------------------------------------------------------- 1 | /** @babel */ 2 | 3 | // This file has shamelessly been procured from 4 | // https://raw.githubusercontent.com/atom/atom/master/spec/async-spec-helpers.js 5 | 6 | export function beforeEach (fn) { 7 | global.beforeEach(function () { 8 | const result = fn() 9 | if (result instanceof Promise) { 10 | waitsForPromise(() => result) 11 | } 12 | }) 13 | } 14 | 15 | export function afterEach (fn) { 16 | global.afterEach(function () { 17 | const result = fn() 18 | if (result instanceof Promise) { 19 | waitsForPromise(() => result) 20 | } 21 | }) 22 | } 23 | 24 | ['it', 'fit', 'ffit', 'fffit'].forEach(function (name) { 25 | module.exports[name] = function (description, fn) { 26 | if (fn === undefined) { 27 | global[name](description) 28 | return 29 | } 30 | 31 | global[name](description, function () { 32 | const result = fn() 33 | if (result instanceof Promise) { 34 | waitsForPromise(() => result) 35 | } 36 | }) 37 | } 38 | }) 39 | 40 | export async function conditionPromise (condition, description = 'anonymous condition') { 41 | const startTime = Date.now() 42 | 43 | while (true) { 44 | await timeoutPromise(100) 45 | 46 | if (await condition()) { 47 | return 48 | } 49 | 50 | if (Date.now() - startTime > 5000) { 51 | throw new Error('Timed out waiting on ' + description) 52 | } 53 | } 54 | } 55 | 56 | export function timeoutPromise (timeout) { 57 | return new Promise(function (resolve) { 58 | global.setTimeout(resolve, timeout) 59 | }) 60 | } 61 | 62 | function waitsForPromise (fn) { 63 | const promise = fn() 64 | global.waitsFor('spec promise to resolve', function (done) { 65 | promise.then(done, function (error) { 66 | jasmine.getEnv().currentSpec.fail(error) 67 | done() 68 | }) 69 | }) 70 | } 71 | 72 | export function emitterEventPromise (emitter, event, timeout = 15000) { 73 | return new Promise((resolve, reject) => { 74 | const timeoutHandle = setTimeout(() => { 75 | reject(new Error(`Timed out waiting for '${event}' event`)) 76 | }, timeout) 77 | emitter.once(event, () => { 78 | clearTimeout(timeoutHandle) 79 | resolve() 80 | }) 81 | }) 82 | } 83 | 84 | export function promisify (original) { 85 | return function (...args) { 86 | return new Promise((resolve, reject) => { 87 | args.push((err, ...results) => { 88 | if (err) { 89 | reject(err) 90 | } else { 91 | resolve(...results) 92 | } 93 | }) 94 | 95 | return original(...args) 96 | }) 97 | } 98 | } 99 | 100 | export function promisifySome (obj, fnNames) { 101 | const result = {} 102 | for (const fnName of fnNames) { 103 | result[fnName] = promisify(obj[fnName]) 104 | } 105 | return result 106 | } 107 | -------------------------------------------------------------------------------- /spec/build-registry-spec.js: -------------------------------------------------------------------------------- 1 | /** @babel */ 2 | 3 | // eslint-disable-next-line no-unused-vars 4 | import { afterEach, beforeEach, it, fit } from './async-spec-helpers' 5 | import { activatePackages } from './spec-helpers' 6 | import { NullBuilder } from './stubs' 7 | 8 | import BuilderRegistry from '../lib/builder-registry' 9 | import BuildState from '../lib/build-state' 10 | 11 | describe('BuilderRegistry', () => { 12 | let builderRegistry 13 | 14 | beforeEach(async () => { 15 | await activatePackages() 16 | 17 | atom.config.set('latex.builder', 'latexmk') 18 | builderRegistry = new BuilderRegistry() 19 | }) 20 | 21 | describe('getBuilderImplementation', () => { 22 | it('returns null when no builders are associated with the given file', () => { 23 | const state = new BuildState('quux.txt') 24 | expect(builderRegistry.getBuilderImplementation(state)).toBeNull() 25 | }) 26 | 27 | it('returns the configured builder when given a regular .tex file', () => { 28 | const state = new BuildState('foo.tex') 29 | expect(builderRegistry.getBuilderImplementation(state).name).toEqual('LatexmkBuilder') 30 | }) 31 | 32 | it('throws an error when unable to resolve ambiguous builder registration', () => { 33 | const allBuilders = builderRegistry.getAllBuilders().push(NullBuilder) 34 | const state = new BuildState('foo.tex') 35 | spyOn(builderRegistry, 'getAllBuilders').andReturn(allBuilders) 36 | expect(() => { builderRegistry.getBuilderImplementation(state) }).toThrow() 37 | }) 38 | 39 | it('returns the Knitr builder when presented with an .Rnw file', () => { 40 | const state = new BuildState('bar.Rnw') 41 | expect(builderRegistry.getBuilderImplementation(state).name).toEqual('KnitrBuilder') 42 | }) 43 | }) 44 | 45 | describe('getBuilder', () => { 46 | beforeEach(() => { 47 | atom.config.set('latex.builder', 'latexmk') 48 | }) 49 | 50 | it('returns null when passed an unhandled file type', () => { 51 | const state = new BuildState('quux.txt') 52 | expect(builderRegistry.getBuilder(state)).toBeNull() 53 | }) 54 | 55 | it('returns a builder instance as configured for regular .tex files', () => { 56 | const state = new BuildState('foo.tex') 57 | expect(builderRegistry.getBuilder(state).constructor.name).toEqual('LatexmkBuilder') 58 | }) 59 | 60 | it('returns a builder instance as configured for knitr files', () => { 61 | const state = new BuildState('bar.Rnw') 62 | expect(builderRegistry.getBuilder(state).constructor.name).toEqual('KnitrBuilder') 63 | }) 64 | }) 65 | }) 66 | -------------------------------------------------------------------------------- /spec/builder-spec.js: -------------------------------------------------------------------------------- 1 | /** @babel */ 2 | 3 | // eslint-disable-next-line no-unused-vars 4 | import { afterEach, beforeEach, it, fit } from './async-spec-helpers' 5 | import { cloneFixtures } from './spec-helpers' 6 | 7 | import path from 'path' 8 | import Builder from '../lib/builder' 9 | import BuildState from '../lib/build-state' 10 | 11 | describe('Builder', () => { 12 | let builder, fixturesPath, filePath, logFilePath, fdbFilePath, state, jobState 13 | 14 | beforeEach(() => { 15 | builder = new Builder() 16 | fixturesPath = cloneFixtures() 17 | filePath = path.join(fixturesPath, 'file.tex') 18 | logFilePath = path.join(fixturesPath, 'file.log') 19 | fdbFilePath = path.join(fixturesPath, 'file.fdb_latexmk') 20 | state = new BuildState(filePath) 21 | state.setOutputDirectory('') 22 | jobState = state.getJobStates()[0] 23 | }) 24 | 25 | describe('constructPath', () => { 26 | it('reads `latex.texPath` as configured', () => { 27 | spyOn(atom.config, 'get').andReturn() 28 | builder.constructPath() 29 | 30 | expect(atom.config.get).toHaveBeenCalledWith('latex.texPath') 31 | }) 32 | 33 | it('uses platform default when `latex.texPath` is not configured', () => { 34 | const defaultTexPath = '/foo/bar' 35 | const expectedPath = [defaultTexPath, process.env.PATH].join(path.delimiter) 36 | atom.config.set('latex.texPath', '') 37 | spyOn(builder, 'defaultTexPath').andReturn(defaultTexPath) 38 | 39 | const constructedPath = builder.constructPath() 40 | expect(constructedPath).toBe(expectedPath) 41 | }) 42 | 43 | it('replaces surrounded $PATH with process.env.PATH', () => { 44 | const texPath = '/foo:$PATH:/bar' 45 | const expectedPath = texPath.replace('$PATH', process.env.PATH) 46 | atom.config.set('latex.texPath', texPath) 47 | 48 | const constructedPath = builder.constructPath() 49 | expect(constructedPath).toBe(expectedPath) 50 | }) 51 | 52 | it('replaces leading $PATH with process.env.PATH', () => { 53 | const texPath = '$PATH:/bar' 54 | const expectedPath = texPath.replace('$PATH', process.env.PATH) 55 | atom.config.set('latex.texPath', texPath) 56 | 57 | const constructedPath = builder.constructPath() 58 | expect(constructedPath).toBe(expectedPath) 59 | }) 60 | 61 | it('replaces trailing $PATH with process.env.PATH', () => { 62 | const texPath = '/foo:$PATH' 63 | const expectedPath = texPath.replace('$PATH', process.env.PATH) 64 | atom.config.set('latex.texPath', texPath) 65 | 66 | const constructedPath = builder.constructPath() 67 | expect(constructedPath).toBe(expectedPath) 68 | }) 69 | 70 | it('prepends process.env.PATH with texPath', () => { 71 | const texPath = '/foo' 72 | const expectedPath = [texPath, process.env.PATH].join(path.delimiter) 73 | atom.config.set('latex.texPath', texPath) 74 | 75 | const constructedPath = builder.constructPath() 76 | expect(constructedPath).toBe(expectedPath) 77 | }) 78 | }) 79 | 80 | describe('parseLogFile', () => { 81 | let logParser 82 | 83 | beforeEach(() => { 84 | logParser = jasmine.createSpyObj('MockLogParser', ['parse']) 85 | spyOn(builder, 'getLogParser').andReturn(logParser) 86 | }) 87 | 88 | it('resolves the associated log file path by invoking @resolveLogFilePath', () => { 89 | spyOn(builder, 'resolveLogFilePath').andReturn('foo.log') 90 | 91 | builder.parseLogFile(jobState) 92 | expect(builder.resolveLogFilePath).toHaveBeenCalledWith(jobState) 93 | }) 94 | 95 | it('does not attempt parse if passed a file path that does not exist', () => { 96 | state.setFilePath('/foo/bar/quux.tex') 97 | builder.parseLogFile(jobState) 98 | 99 | expect(logParser.parse).not.toHaveBeenCalled() 100 | }) 101 | 102 | it('attempts to parse the resolved log file', () => { 103 | builder.parseLogFile(jobState) 104 | 105 | expect(builder.getLogParser).toHaveBeenCalledWith(logFilePath, filePath) 106 | expect(logParser.parse).toHaveBeenCalled() 107 | }) 108 | }) 109 | 110 | describe('parseFdbFile', () => { 111 | let fdbParser 112 | 113 | beforeEach(() => { 114 | fdbParser = jasmine.createSpyObj('MockFdbParser', ['parse']) 115 | spyOn(builder, 'getFdbParser').andReturn(fdbParser) 116 | }) 117 | 118 | it('resolves the associated fdb file path by invoking @resolveFdbFilePath', () => { 119 | spyOn(builder, 'resolveFdbFilePath').andReturn('foo.fdb_latexmk') 120 | 121 | builder.parseFdbFile(jobState) 122 | expect(builder.resolveFdbFilePath).toHaveBeenCalledWith(jobState) 123 | }) 124 | 125 | it('does not attempt parse if passed a file path that does not exist', () => { 126 | state.setFilePath('/foo/bar/quux.tex') 127 | builder.parseFdbFile(jobState) 128 | 129 | expect(fdbParser.parse).not.toHaveBeenCalled() 130 | }) 131 | 132 | it('attempts to parse the resolved fdb file', () => { 133 | builder.parseFdbFile(jobState) 134 | 135 | expect(builder.getFdbParser).toHaveBeenCalledWith(fdbFilePath) 136 | expect(fdbParser.parse).toHaveBeenCalled() 137 | }) 138 | }) 139 | 140 | describe('parseLogAndFdbFiles', () => { 141 | it('verifies that the correct output file is selected when using various latexmk modes', () => { 142 | const switches = [ 143 | { name: 'pdf', format: 'pdf' }, 144 | { name: 'pdfdvi', format: 'pdf' }, 145 | { name: 'pdfps', format: 'pdf' }, 146 | { name: 'ps', format: 'ps' }, 147 | { name: 'dvi', format: 'dvi' }] 148 | state.setOutputDirectory('log-parse') 149 | 150 | for (const { name, format } of switches) { 151 | state.setJobNames([`file-${name}`]) 152 | jobState = state.getJobStates()[0] 153 | builder.parseLogAndFdbFiles(jobState) 154 | expect(path.basename(jobState.getOutputFilePath())).toBe(`${jobState.getJobName()}.${format}`, 155 | `Select ${format} file when using -${name} switch.`) 156 | } 157 | }) 158 | }) 159 | }) 160 | -------------------------------------------------------------------------------- /spec/builders/knitr-spec.js: -------------------------------------------------------------------------------- 1 | /** @babel */ 2 | 3 | // eslint-disable-next-line no-unused-vars 4 | import { afterEach, beforeEach, it, fit } from '../async-spec-helpers' 5 | import { activatePackages, cloneFixtures } from '../spec-helpers' 6 | 7 | import fs from 'fs-plus' 8 | import path from 'path' 9 | import KnitrBuilder from '../../lib/builders/knitr' 10 | import BuildState from '../../lib/build-state' 11 | 12 | function getRawFile (filePath) { 13 | return fs.readFileSync(filePath, { encoding: 'utf-8' }) 14 | } 15 | 16 | describe('KnitrBuilder', () => { 17 | let builder, fixturesPath, filePath, state, jobState 18 | 19 | beforeEach(async () => { 20 | await activatePackages() 21 | 22 | builder = new KnitrBuilder() 23 | spyOn(builder, 'logStatusCode').andCallThrough() 24 | 25 | fixturesPath = cloneFixtures() 26 | filePath = path.join(fixturesPath, 'knitr', 'file.Rnw') 27 | 28 | state = new BuildState(filePath) 29 | state.setEngine('pdflatex') 30 | state.setOutputFormat('pdf') 31 | state.setOutputDirectory('') 32 | jobState = state.getJobStates()[0] 33 | }) 34 | 35 | describe('constructArgs', () => { 36 | it('produces default arguments containing expected file path', () => { 37 | const expectedArgs = [ 38 | '-e "library(knitr)"', 39 | '-e "opts_knit$set(concordance = TRUE)"', 40 | `-e "knit('${filePath.replace(/\\/g, '\\\\')}')"` 41 | ] 42 | 43 | const args = builder.constructArgs(jobState) 44 | expect(args).toEqual(expectedArgs) 45 | }) 46 | }) 47 | 48 | describe('constructPatchSynctexArgs', () => { 49 | it('produces default arguments containing expected file path', () => { 50 | const escapedFilePath = filePath.replace(/\\/g, '\\\\') 51 | const escapedSynctexPath = escapedFilePath.replace(/\.[^.]+$/, '') 52 | const expectedArgs = [ 53 | '-e "library(patchSynctex)"', 54 | `-e "patchSynctex('${escapedFilePath}',syncfile='${escapedSynctexPath}')"` 55 | ] 56 | 57 | const args = builder.constructPatchSynctexArgs(jobState) 58 | expect(args).toEqual(expectedArgs) 59 | }) 60 | }) 61 | 62 | describe('run', () => { 63 | it('successfully executes knitr when given a valid R Sweave file', async () => { 64 | const outputFilePath = path.join(fixturesPath, 'knitr', 'file.tex') 65 | 66 | const exitCode = await builder.run(jobState) 67 | 68 | expect(exitCode).toBe(0) 69 | expect(builder.logStatusCode).not.toHaveBeenCalled() 70 | expect(getRawFile(outputFilePath)).toContain('$\\tau \\approx 6.2831853$') 71 | }) 72 | 73 | it('fails to execute knitr when given an invalid file path', async () => { 74 | filePath = path.join(fixturesPath, 'foo.Rnw') 75 | state.setFilePath(filePath) 76 | 77 | const exitCode = await builder.run(jobState) 78 | 79 | runs(() => { 80 | expect(exitCode).toBe(1) 81 | expect(builder.logStatusCode).toHaveBeenCalled() 82 | }) 83 | }) 84 | 85 | it('detects missing knitr library and logs an error', async () => { 86 | const directoryPath = path.dirname(filePath) 87 | const env = { 'R_LIBS_USER': '/dev/null', 'R_LIBS_SITE': '/dev/null' } 88 | const options = builder.constructChildProcessOptions(directoryPath) 89 | Object.assign(options.env, env) 90 | spyOn(builder, 'constructChildProcessOptions').andReturn(options) 91 | spyOn(latex.log, 'showMessages').andCallThrough() 92 | 93 | const exitCode = await builder.run(jobState) 94 | 95 | expect(exitCode).toBe(-1) 96 | expect(builder.logStatusCode).toHaveBeenCalled() 97 | expect(latex.log.showMessages).toHaveBeenCalledWith([{ 98 | type: 'error', 99 | text: 'The R package "knitr" could not be loaded.' 100 | }]) 101 | }) 102 | }) 103 | 104 | describe('resolveOutputPath', () => { 105 | let sourcePath, resultPath 106 | 107 | beforeEach(() => { 108 | sourcePath = path.resolve('/var/foo.Rnw') 109 | resultPath = path.resolve('/var/foo.tex') 110 | }) 111 | 112 | it('detects an absolute path and returns it unchanged', () => { 113 | const stdout = `foo\nbar\n\n[1] "${resultPath}"` 114 | const resolvedPath = builder.resolveOutputPath(sourcePath, stdout) 115 | 116 | expect(resolvedPath).toBe(resultPath) 117 | }) 118 | 119 | it('detects a relative path and makes it absolute with respect to the source file', () => { 120 | const stdout = `foo\nbar\n\n[1] "${path.basename(resultPath)}"` 121 | const resolvedPath = builder.resolveOutputPath(sourcePath, stdout) 122 | 123 | expect(resolvedPath).toBe(resultPath) 124 | }) 125 | }) 126 | 127 | describe('canProcess', () => { 128 | it('returns true when given a file path with a .Rnw extension', () => { 129 | const canProcess = KnitrBuilder.canProcess(state) 130 | expect(canProcess).toBe(true) 131 | }) 132 | }) 133 | }) 134 | -------------------------------------------------------------------------------- /spec/fixtures/_minted-wibble/default.pygstyle: -------------------------------------------------------------------------------- 1 | 2 | \makeatletter 3 | \def\PYGdefault@reset{\let\PYGdefault@it=\relax \let\PYGdefault@bf=\relax% 4 | \let\PYGdefault@ul=\relax \let\PYGdefault@tc=\relax% 5 | \let\PYGdefault@bc=\relax \let\PYGdefault@ff=\relax} 6 | \def\PYGdefault@tok#1{\csname PYGdefault@tok@#1\endcsname} 7 | \def\PYGdefault@toks#1+{\ifx\relax#1\empty\else% 8 | \PYGdefault@tok{#1}\expandafter\PYGdefault@toks\fi} 9 | \def\PYGdefault@do#1{\PYGdefault@bc{\PYGdefault@tc{\PYGdefault@ul{% 10 | \PYGdefault@it{\PYGdefault@bf{\PYGdefault@ff{#1}}}}}}} 11 | \def\PYGdefault#1#2{\PYGdefault@reset\PYGdefault@toks#1+\relax+\PYGdefault@do{#2}} 12 | 13 | \expandafter\def\csname PYGdefault@tok@sb\endcsname{\def\PYGdefault@tc##1{\textcolor[rgb]{0.73,0.13,0.13}{##1}}} 14 | \expandafter\def\csname PYGdefault@tok@mh\endcsname{\def\PYGdefault@tc##1{\textcolor[rgb]{0.40,0.40,0.40}{##1}}} 15 | \expandafter\def\csname PYGdefault@tok@sh\endcsname{\def\PYGdefault@tc##1{\textcolor[rgb]{0.73,0.13,0.13}{##1}}} 16 | \expandafter\def\csname PYGdefault@tok@sd\endcsname{\let\PYGdefault@it=\textit\def\PYGdefault@tc##1{\textcolor[rgb]{0.73,0.13,0.13}{##1}}} 17 | \expandafter\def\csname PYGdefault@tok@gu\endcsname{\let\PYGdefault@bf=\textbf\def\PYGdefault@tc##1{\textcolor[rgb]{0.50,0.00,0.50}{##1}}} 18 | \expandafter\def\csname PYGdefault@tok@o\endcsname{\def\PYGdefault@tc##1{\textcolor[rgb]{0.40,0.40,0.40}{##1}}} 19 | \expandafter\def\csname PYGdefault@tok@nt\endcsname{\let\PYGdefault@bf=\textbf\def\PYGdefault@tc##1{\textcolor[rgb]{0.00,0.50,0.00}{##1}}} 20 | \expandafter\def\csname PYGdefault@tok@bp\endcsname{\def\PYGdefault@tc##1{\textcolor[rgb]{0.00,0.50,0.00}{##1}}} 21 | \expandafter\def\csname PYGdefault@tok@nd\endcsname{\def\PYGdefault@tc##1{\textcolor[rgb]{0.67,0.13,1.00}{##1}}} 22 | \expandafter\def\csname PYGdefault@tok@ow\endcsname{\let\PYGdefault@bf=\textbf\def\PYGdefault@tc##1{\textcolor[rgb]{0.67,0.13,1.00}{##1}}} 23 | \expandafter\def\csname PYGdefault@tok@ni\endcsname{\let\PYGdefault@bf=\textbf\def\PYGdefault@tc##1{\textcolor[rgb]{0.60,0.60,0.60}{##1}}} 24 | \expandafter\def\csname PYGdefault@tok@c\endcsname{\let\PYGdefault@it=\textit\def\PYGdefault@tc##1{\textcolor[rgb]{0.25,0.50,0.50}{##1}}} 25 | \expandafter\def\csname PYGdefault@tok@sr\endcsname{\def\PYGdefault@tc##1{\textcolor[rgb]{0.73,0.40,0.53}{##1}}} 26 | \expandafter\def\csname PYGdefault@tok@kc\endcsname{\let\PYGdefault@bf=\textbf\def\PYGdefault@tc##1{\textcolor[rgb]{0.00,0.50,0.00}{##1}}} 27 | \expandafter\def\csname PYGdefault@tok@gp\endcsname{\let\PYGdefault@bf=\textbf\def\PYGdefault@tc##1{\textcolor[rgb]{0.00,0.00,0.50}{##1}}} 28 | \expandafter\def\csname PYGdefault@tok@gh\endcsname{\let\PYGdefault@bf=\textbf\def\PYGdefault@tc##1{\textcolor[rgb]{0.00,0.00,0.50}{##1}}} 29 | \expandafter\def\csname PYGdefault@tok@cpf\endcsname{\let\PYGdefault@it=\textit\def\PYGdefault@tc##1{\textcolor[rgb]{0.25,0.50,0.50}{##1}}} 30 | \expandafter\def\csname PYGdefault@tok@ss\endcsname{\def\PYGdefault@tc##1{\textcolor[rgb]{0.10,0.09,0.49}{##1}}} 31 | \expandafter\def\csname PYGdefault@tok@cp\endcsname{\def\PYGdefault@tc##1{\textcolor[rgb]{0.74,0.48,0.00}{##1}}} 32 | \expandafter\def\csname PYGdefault@tok@c1\endcsname{\let\PYGdefault@it=\textit\def\PYGdefault@tc##1{\textcolor[rgb]{0.25,0.50,0.50}{##1}}} 33 | \expandafter\def\csname PYGdefault@tok@kt\endcsname{\def\PYGdefault@tc##1{\textcolor[rgb]{0.69,0.00,0.25}{##1}}} 34 | \expandafter\def\csname PYGdefault@tok@sc\endcsname{\def\PYGdefault@tc##1{\textcolor[rgb]{0.73,0.13,0.13}{##1}}} 35 | \expandafter\def\csname PYGdefault@tok@mb\endcsname{\def\PYGdefault@tc##1{\textcolor[rgb]{0.40,0.40,0.40}{##1}}} 36 | \expandafter\def\csname PYGdefault@tok@m\endcsname{\def\PYGdefault@tc##1{\textcolor[rgb]{0.40,0.40,0.40}{##1}}} 37 | \expandafter\def\csname PYGdefault@tok@nl\endcsname{\def\PYGdefault@tc##1{\textcolor[rgb]{0.63,0.63,0.00}{##1}}} 38 | \expandafter\def\csname PYGdefault@tok@mf\endcsname{\def\PYGdefault@tc##1{\textcolor[rgb]{0.40,0.40,0.40}{##1}}} 39 | \expandafter\def\csname PYGdefault@tok@s2\endcsname{\def\PYGdefault@tc##1{\textcolor[rgb]{0.73,0.13,0.13}{##1}}} 40 | \expandafter\def\csname PYGdefault@tok@ch\endcsname{\let\PYGdefault@it=\textit\def\PYGdefault@tc##1{\textcolor[rgb]{0.25,0.50,0.50}{##1}}} 41 | \expandafter\def\csname PYGdefault@tok@ge\endcsname{\let\PYGdefault@it=\textit} 42 | \expandafter\def\csname PYGdefault@tok@cs\endcsname{\let\PYGdefault@it=\textit\def\PYGdefault@tc##1{\textcolor[rgb]{0.25,0.50,0.50}{##1}}} 43 | \expandafter\def\csname PYGdefault@tok@vg\endcsname{\def\PYGdefault@tc##1{\textcolor[rgb]{0.10,0.09,0.49}{##1}}} 44 | \expandafter\def\csname PYGdefault@tok@vc\endcsname{\def\PYGdefault@tc##1{\textcolor[rgb]{0.10,0.09,0.49}{##1}}} 45 | \expandafter\def\csname PYGdefault@tok@kn\endcsname{\let\PYGdefault@bf=\textbf\def\PYGdefault@tc##1{\textcolor[rgb]{0.00,0.50,0.00}{##1}}} 46 | \expandafter\def\csname PYGdefault@tok@nc\endcsname{\let\PYGdefault@bf=\textbf\def\PYGdefault@tc##1{\textcolor[rgb]{0.00,0.00,1.00}{##1}}} 47 | \expandafter\def\csname PYGdefault@tok@no\endcsname{\def\PYGdefault@tc##1{\textcolor[rgb]{0.53,0.00,0.00}{##1}}} 48 | \expandafter\def\csname PYGdefault@tok@k\endcsname{\let\PYGdefault@bf=\textbf\def\PYGdefault@tc##1{\textcolor[rgb]{0.00,0.50,0.00}{##1}}} 49 | \expandafter\def\csname PYGdefault@tok@nb\endcsname{\def\PYGdefault@tc##1{\textcolor[rgb]{0.00,0.50,0.00}{##1}}} 50 | \expandafter\def\csname PYGdefault@tok@vi\endcsname{\def\PYGdefault@tc##1{\textcolor[rgb]{0.10,0.09,0.49}{##1}}} 51 | \expandafter\def\csname PYGdefault@tok@gt\endcsname{\def\PYGdefault@tc##1{\textcolor[rgb]{0.00,0.27,0.87}{##1}}} 52 | \expandafter\def\csname PYGdefault@tok@gr\endcsname{\def\PYGdefault@tc##1{\textcolor[rgb]{1.00,0.00,0.00}{##1}}} 53 | \expandafter\def\csname PYGdefault@tok@nv\endcsname{\def\PYGdefault@tc##1{\textcolor[rgb]{0.10,0.09,0.49}{##1}}} 54 | \expandafter\def\csname PYGdefault@tok@go\endcsname{\def\PYGdefault@tc##1{\textcolor[rgb]{0.53,0.53,0.53}{##1}}} 55 | \expandafter\def\csname PYGdefault@tok@kd\endcsname{\let\PYGdefault@bf=\textbf\def\PYGdefault@tc##1{\textcolor[rgb]{0.00,0.50,0.00}{##1}}} 56 | \expandafter\def\csname PYGdefault@tok@nn\endcsname{\let\PYGdefault@bf=\textbf\def\PYGdefault@tc##1{\textcolor[rgb]{0.00,0.00,1.00}{##1}}} 57 | \expandafter\def\csname PYGdefault@tok@kp\endcsname{\def\PYGdefault@tc##1{\textcolor[rgb]{0.00,0.50,0.00}{##1}}} 58 | \expandafter\def\csname PYGdefault@tok@se\endcsname{\let\PYGdefault@bf=\textbf\def\PYGdefault@tc##1{\textcolor[rgb]{0.73,0.40,0.13}{##1}}} 59 | \expandafter\def\csname PYGdefault@tok@cm\endcsname{\let\PYGdefault@it=\textit\def\PYGdefault@tc##1{\textcolor[rgb]{0.25,0.50,0.50}{##1}}} 60 | \expandafter\def\csname PYGdefault@tok@gi\endcsname{\def\PYGdefault@tc##1{\textcolor[rgb]{0.00,0.63,0.00}{##1}}} 61 | \expandafter\def\csname PYGdefault@tok@err\endcsname{\def\PYGdefault@bc##1{\setlength{\fboxsep}{0pt}\fcolorbox[rgb]{1.00,0.00,0.00}{1,1,1}{\strut ##1}}} 62 | \expandafter\def\csname PYGdefault@tok@il\endcsname{\def\PYGdefault@tc##1{\textcolor[rgb]{0.40,0.40,0.40}{##1}}} 63 | \expandafter\def\csname PYGdefault@tok@s\endcsname{\def\PYGdefault@tc##1{\textcolor[rgb]{0.73,0.13,0.13}{##1}}} 64 | \expandafter\def\csname PYGdefault@tok@si\endcsname{\let\PYGdefault@bf=\textbf\def\PYGdefault@tc##1{\textcolor[rgb]{0.73,0.40,0.53}{##1}}} 65 | \expandafter\def\csname PYGdefault@tok@gd\endcsname{\def\PYGdefault@tc##1{\textcolor[rgb]{0.63,0.00,0.00}{##1}}} 66 | \expandafter\def\csname PYGdefault@tok@s1\endcsname{\def\PYGdefault@tc##1{\textcolor[rgb]{0.73,0.13,0.13}{##1}}} 67 | \expandafter\def\csname PYGdefault@tok@sx\endcsname{\def\PYGdefault@tc##1{\textcolor[rgb]{0.00,0.50,0.00}{##1}}} 68 | \expandafter\def\csname PYGdefault@tok@na\endcsname{\def\PYGdefault@tc##1{\textcolor[rgb]{0.49,0.56,0.16}{##1}}} 69 | \expandafter\def\csname PYGdefault@tok@kr\endcsname{\let\PYGdefault@bf=\textbf\def\PYGdefault@tc##1{\textcolor[rgb]{0.00,0.50,0.00}{##1}}} 70 | \expandafter\def\csname PYGdefault@tok@w\endcsname{\def\PYGdefault@tc##1{\textcolor[rgb]{0.73,0.73,0.73}{##1}}} 71 | \expandafter\def\csname PYGdefault@tok@nf\endcsname{\def\PYGdefault@tc##1{\textcolor[rgb]{0.00,0.00,1.00}{##1}}} 72 | \expandafter\def\csname PYGdefault@tok@gs\endcsname{\let\PYGdefault@bf=\textbf} 73 | \expandafter\def\csname PYGdefault@tok@ne\endcsname{\let\PYGdefault@bf=\textbf\def\PYGdefault@tc##1{\textcolor[rgb]{0.82,0.25,0.23}{##1}}} 74 | \expandafter\def\csname PYGdefault@tok@mi\endcsname{\def\PYGdefault@tc##1{\textcolor[rgb]{0.40,0.40,0.40}{##1}}} 75 | \expandafter\def\csname PYGdefault@tok@mo\endcsname{\def\PYGdefault@tc##1{\textcolor[rgb]{0.40,0.40,0.40}{##1}}} 76 | 77 | \def\PYGdefaultZbs{\char`\\} 78 | \def\PYGdefaultZus{\char`\_} 79 | \def\PYGdefaultZob{\char`\{} 80 | \def\PYGdefaultZcb{\char`\}} 81 | \def\PYGdefaultZca{\char`\^} 82 | \def\PYGdefaultZam{\char`\&} 83 | \def\PYGdefaultZlt{\char`\<} 84 | \def\PYGdefaultZgt{\char`\>} 85 | \def\PYGdefaultZsh{\char`\#} 86 | \def\PYGdefaultZpc{\char`\%} 87 | \def\PYGdefaultZdl{\char`\$} 88 | \def\PYGdefaultZhy{\char`\-} 89 | \def\PYGdefaultZsq{\char`\'} 90 | \def\PYGdefaultZdq{\char`\"} 91 | \def\PYGdefaultZti{\char`\~} 92 | % for compatibility with earlier versions 93 | \def\PYGdefaultZat{@} 94 | \def\PYGdefaultZlb{[} 95 | \def\PYGdefaultZrb{]} 96 | \makeatother 97 | 98 | -------------------------------------------------------------------------------- /spec/fixtures/error-warning.tex: -------------------------------------------------------------------------------- 1 | % !TEX jobnames = foo bar, snafu 2 | \documentclass{article} 3 | 4 | \begin{document} 5 | 6 | \begin{center} 7 | % Error: There's no line here to end 8 | \hbox{Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim 9 | veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate 10 | velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim 11 | id est laborum} \\ 12 | \end{center} 13 | 14 | % Error: Argument of \@sect has an extra }. 15 | % Error: Paragraph ended before \@sect was complete. 16 | \section{Foo\footnote{Bar}} 17 | 18 | Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim 19 | veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate 20 | velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim 21 | id est laborum 22 | 23 | \begin{tabular}{ll} 24 | % Extra alignment tab has bee changed to \cr 25 | Foo & Bar & Snafu \\ 26 | \end{tabular} 27 | 28 | % Generate errors and warnings in a subfile with spaces 29 | \input{"sub/foo bar.tex"} 30 | 31 | % Generate errors and warnings in a subfile located in another directory 32 | \input{sub/wibble} 33 | 34 | \end{document} 35 | -------------------------------------------------------------------------------- /spec/fixtures/errors.log: -------------------------------------------------------------------------------- 1 | This is pdfTeX, Version 3.14159265-2.6-1.40.15 (TeX Live 2014) (preloaded format=pdflatex 2014.9.23) 11 OCT 2014 10:14 2 | entering extended mode 3 | \write18 enabled. 4 | file:line:error style messages enabled. 5 | %&-line parsing enabled. 6 | **errors.tex 7 | (./errors.tex 8 | LaTeX2e <2014/05/01> 9 | Babel <3.9k> and hyphenation patterns for 79 languages loaded. 10 | (/usr/local/texlive/2014/texmf-dist/tex/latex/base/article.cls 11 | Document Class: article 2007/10/19 v1.4h Standard LaTeX document class 12 | (/usr/local/texlive/2014/texmf-dist/tex/latex/base/size10.clo 13 | File: size10.clo 2007/10/19 v1.4h Standard LaTeX file (size option) 14 | ) 15 | \c@part=\count79 16 | \c@section=\count80 17 | \c@subsection=\count81 18 | \c@subsubsection=\count82 19 | \c@paragraph=\count83 20 | \c@subparagraph=\count84 21 | \c@figure=\count85 22 | \c@table=\count86 23 | \abovecaptionskip=\skip41 24 | \belowcaptionskip=\skip42 25 | \bibindent=\dimen102 26 | ) (/usr/local/texlive/2014/texmf-dist/tex/latex/mathtools/mathtools.sty 27 | Package: mathtools 2014/07/16 v1.15 mathematical typesetting tools 28 | (/usr/local/texlive/2014/texmf-dist/tex/latex/graphics/keyval.sty 29 | Package: keyval 2014/05/08 v1.15 key=value parser (DPC) 30 | \KV@toks@=\toks14 31 | ) (/usr/local/texlive/2014/texmf-dist/tex/latex/tools/calc.sty 32 | Package: calc 2007/08/22 v4.3 Infix arithmetic (KKT,FJ) 33 | \calc@Acount=\count87 34 | \calc@Bcount=\count88 35 | \calc@Adimen=\dimen103 36 | \calc@Bdimen=\dimen104 37 | \calc@Askip=\skip43 38 | \calc@Bskip=\skip44 39 | LaTeX Info: Redefining \setlength on input line 75. 40 | LaTeX Info: Redefining \addtolength on input line 76. 41 | \calc@Ccount=\count89 42 | \calc@Cskip=\skip45 43 | ) (/usr/local/texlive/2014/texmf-dist/tex/latex/mathtools/mhsetup.sty 44 | Package: mhsetup 2010/01/21 v1.2a programming setup (MH) 45 | ) (/usr/local/texlive/2014/texmf-dist/tex/latex/amsmath/amsmath.sty 46 | Package: amsmath 2013/01/14 v2.14 AMS math features 47 | \@mathmargin=\skip46 48 | 49 | For additional information on amsmath, use the `?' option. 50 | (/usr/local/texlive/2014/texmf-dist/tex/latex/amsmath/amstext.sty 51 | Package: amstext 2000/06/29 v2.01 52 | (/usr/local/texlive/2014/texmf-dist/tex/latex/amsmath/amsgen.sty 53 | File: amsgen.sty 1999/11/30 v2.0 54 | \@emptytoks=\toks15 55 | \ex@=\dimen105 56 | )) (/usr/local/texlive/2014/texmf-dist/tex/latex/amsmath/amsbsy.sty 57 | Package: amsbsy 1999/11/29 v1.2d 58 | \pmbraise@=\dimen106 59 | ) (/usr/local/texlive/2014/texmf-dist/tex/latex/amsmath/amsopn.sty 60 | Package: amsopn 1999/12/14 v2.01 operator names 61 | ) 62 | \inf@bad=\count90 63 | LaTeX Info: Redefining \frac on input line 210. 64 | \uproot@=\count91 65 | \leftroot@=\count92 66 | LaTeX Info: Redefining \overline on input line 306. 67 | \classnum@=\count93 68 | \DOTSCASE@=\count94 69 | LaTeX Info: Redefining \ldots on input line 378. 70 | LaTeX Info: Redefining \dots on input line 381. 71 | LaTeX Info: Redefining \cdots on input line 466. 72 | \Mathstrutbox@=\box26 73 | \strutbox@=\box27 74 | \big@size=\dimen107 75 | LaTeX Font Info: Redeclaring font encoding OML on input line 566. 76 | LaTeX Font Info: Redeclaring font encoding OMS on input line 567. 77 | \macc@depth=\count95 78 | \c@MaxMatrixCols=\count96 79 | \dotsspace@=\muskip10 80 | \c@parentequation=\count97 81 | \dspbrk@lvl=\count98 82 | \tag@help=\toks16 83 | \row@=\count99 84 | \column@=\count100 85 | \maxfields@=\count101 86 | \andhelp@=\toks17 87 | \eqnshift@=\dimen108 88 | \alignsep@=\dimen109 89 | \tagshift@=\dimen110 90 | \tagwidth@=\dimen111 91 | \totwidth@=\dimen112 92 | \lineht@=\dimen113 93 | \@envbody=\toks18 94 | \multlinegap=\skip47 95 | \multlinetaggap=\skip48 96 | \mathdisplay@stack=\toks19 97 | LaTeX Info: Redefining \[ on input line 2665. 98 | LaTeX Info: Redefining \] on input line 2666. 99 | ) 100 | LaTeX Info: Thecontrolsequence`\['isalreadyrobust on input line 129. 101 | LaTeX Info: Thecontrolsequence`\]'isalreadyrobust on input line 129. 102 | \g_MT_multlinerow_int=\count102 103 | \l_MT_multwidth_dim=\dimen114 104 | \origjot=\skip49 105 | \l_MT_shortvdotswithinadjustabove_dim=\dimen115 106 | \l_MT_shortvdotswithinadjustbelow_dim=\dimen116 107 | \l_MT_above_intertext_sep=\dimen117 108 | \l_MT_below_intertext_sep=\dimen118 109 | \l_MT_above_shortintertext_sep=\dimen119 110 | \l_MT_below_shortintertext_sep=\dimen120 111 | ) (/foo/output/errors.aux) 112 | \openout1 = `errors.aux'. 113 | 114 | LaTeX Font Info: Checking defaults for OML/cmm/m/it on input line 5. 115 | LaTeX Font Info: ... okay on input line 5. 116 | LaTeX Font Info: Checking defaults for T1/cmr/m/n on input line 5. 117 | LaTeX Font Info: ... okay on input line 5. 118 | LaTeX Font Info: Checking defaults for OT1/cmr/m/n on input line 5. 119 | LaTeX Font Info: ... okay on input line 5. 120 | LaTeX Font Info: Checking defaults for OMS/cmsy/m/n on input line 5. 121 | LaTeX Font Info: ... okay on input line 5. 122 | LaTeX Font Info: Checking defaults for OMX/cmex/m/n on input line 5. 123 | LaTeX Font Info: ... okay on input line 5. 124 | LaTeX Font Info: Checking defaults for U/cmr/m/n on input line 5. 125 | LaTeX Font Info: ... okay on input line 5. 126 | (/usr/local/texlive/2014/texmf-dist/tex/latex/graphics/graphicx.sty 127 | Package: graphicx 2014/04/25 v1.0g Enhanced LaTeX Graphics (DPC,SPQR) 128 | (/usr/local/texlive/2014/texmf-dist/tex/latex/graphics/graphics.sty 129 | Package: graphics 2009/02/05 v1.0o Standard LaTeX Graphics (DPC,SPQR) 130 | (/usr/local/texlive/2014/texmf-dist/tex/latex/graphics/trig.sty 131 | Package: trig 1999/03/16 v1.09 sin cos tan (DPC) 132 | ) (/usr/local/texlive/2014/texmf-dist/tex/latex/latexconfig/graphics.cfg 133 | File: graphics.cfg 2010/04/23 v1.9 graphics configuration of TeX Live 134 | ) 135 | Package graphics Info: Driver file: pdftex.def on input line 91. 136 | (/usr/local/texlive/2014/texmf-dist/tex/latex/pdftex-def/pdftex.def 137 | File: pdftex.def 2011/05/27 v0.06d Graphics/color for pdfTeX 138 | (/usr/local/texlive/2014/texmf-dist/tex/generic/oberdiek/infwarerr.sty 139 | Package: infwarerr 2010/04/08 v1.3 Providing info/warning/error messages (HO) 140 | ) (/usr/local/texlive/2014/texmf-dist/tex/generic/oberdiek/ltxcmds.sty 141 | Package: ltxcmds 2011/11/09 v1.22 LaTeX kernel commands for general use (HO) 142 | ) 143 | \Gread@gobject=\count103 144 | (/usr/local/texlive/2014/texmf-dist/tex/context/base/supp-pdf.mkii 145 | [Loading MPS to PDF converter (version 2006.09.02).] 146 | \scratchcounter=\count104 147 | \scratchdimen=\dimen121 148 | \scratchbox=\box28 149 | \nofMPsegments=\count105 150 | \nofMParguments=\count106 151 | \everyMPshowfont=\toks20 152 | \MPscratchCnt=\count107 153 | \MPscratchDim=\dimen122 154 | \MPnumerator=\count108 155 | \makeMPintoPDFobject=\count109 156 | \everyMPtoPDFconversion=\toks21 157 | ))) (/usr/local/texlive/2014/texmf-dist/tex/generic/oberdiek/pdftexcmds.sty 158 | Package: pdftexcmds 2011/11/29 v0.20 Utility functions of pdfTeX for LuaTeX (HO) 159 | (/usr/local/texlive/2014/texmf-dist/tex/generic/oberdiek/ifluatex.sty 160 | Package: ifluatex 2010/03/01 v1.3 Provides the ifluatex switch (HO) 161 | Package ifluatex Info: LuaTeX not detected. 162 | ) (/usr/local/texlive/2014/texmf-dist/tex/generic/oberdiek/ifpdf.sty 163 | Package: ifpdf 2011/01/30 v2.3 Provides the ifpdf switch (HO) 164 | Package ifpdf Info: pdfTeX in PDF mode is detected. 165 | ) 166 | Package pdftexcmds Info: LuaTeX not detected. 167 | Package pdftexcmds Info: \pdf@primitive is available. 168 | Package pdftexcmds Info: \pdf@ifprimitive is available. 169 | Package pdftexcmds Info: \pdfdraftmode found. 170 | ) (/usr/local/texlive/2014/texmf-dist/tex/latex/oberdiek/epstopdf-base.sty 171 | Package: epstopdf-base 2010/02/09 v2.5 Base part for package epstopdf 172 | (/usr/local/texlive/2014/texmf-dist/tex/latex/oberdiek/grfext.sty 173 | Package: grfext 2010/08/19 v1.1 Manage graphics extensions (HO) 174 | (/usr/local/texlive/2014/texmf-dist/tex/generic/oberdiek/kvdefinekeys.sty 175 | Package: kvdefinekeys 2011/04/07 v1.3 Define keys (HO) 176 | )) (/usr/local/texlive/2014/texmf-dist/tex/latex/oberdiek/kvoptions.sty 177 | Package: kvoptions 2011/06/30 v3.11 Key value format for package options (HO) 178 | (/usr/local/texlive/2014/texmf-dist/tex/generic/oberdiek/kvsetkeys.sty 179 | Package: kvsetkeys 2012/04/25 v1.16 Key value parser (HO) 180 | (/usr/local/texlive/2014/texmf-dist/tex/generic/oberdiek/etexcmds.sty 181 | Package: etexcmds 2011/02/16 v1.5 Avoid name clashes with e-TeX commands (HO) 182 | Package etexcmds Info: Could not find \expanded. 183 | (etexcmds) That can mean that you are not using pdfTeX 1.50 or 184 | (etexcmds) that some package has redefined \expanded. 185 | (etexcmds) In the latter case, load this package earlier. 186 | ))) 187 | Package grfext Info: Graphics extension search list: 188 | (grfext) [.png,.pdf,.jpg,.mps,.jpeg,.jbig2,.jb2,.PNG,.PDF,.JPG,.JPEG,.JBIG2,.JB2,.eps] 189 | (grfext) \AppendGraphicsExtensions on input line 452. 190 | (/usr/local/texlive/2014/texmf-dist/tex/latex/latexconfig/epstopdf-sys.cfg 191 | File: epstopdf-sys.cfg 2010/07/13 v1.3 Configuration of (r)epstopdf for TeX Live 192 | )) 193 | \Gin@req@height=\dimen123 194 | \Gin@req@width=\dimen124 195 | ) 196 | 197 | ./errors.tex:10: LaTeX Error: \begin{gather*} on input line 8 ended by \end{gather}. 198 | 199 | See the LaTeX manual or LaTeX Companion for explanation. 200 | Type H for immediate help. 201 | ... 202 | 203 | l.10 \end{gather} 204 | 205 | Your command was ignored. 206 | Type I to replace it with another command, 207 | or to continue without it. 208 | 209 | 210 | ./errors.tex:12: LaTeX Error: Environment foo undefined. 211 | 212 | See the LaTeX manual or LaTeX Companion for explanation. 213 | Type H for immediate help. 214 | ... 215 | 216 | l.12 \begin{foo} 217 | \end{foo} 218 | Your command was ignored. 219 | Type I to replace it with another command, 220 | or to continue without it. 221 | 222 | 223 | ./errors.tex:12: LaTeX Error: \begin{document} ended by \end{foo}. 224 | 225 | See the LaTeX manual or LaTeX Companion for explanation. 226 | Type H for immediate help. 227 | ... 228 | 229 | l.12 \begin{foo}\end{foo} 230 | 231 | Your command was ignored. 232 | Type I to replace it with another command, 233 | or to continue without it. 234 | 235 | [1 236 | 237 | {/usr/local/texlive/2014/texmf-var/fonts/map/pdftex/updmap/pdftex.map}] (/foo/output/errors.aux) ) 238 | Here is how much of TeX's memory you used: 239 | 2617 strings out of 493108 240 | 37850 string characters out of 6134847 241 | 100329 words of memory out of 5000000 242 | 6070 multiletter control sequences out of 15000+600000 243 | 5846 words of font info for 22 fonts, out of 8000000 for 9000 244 | 1141 hyphenation exceptions out of 8191 245 | 51i,7n,27p,253b,119s stack positions out of 5000i,500n,10000p,200000b,80000s 246 | 247 | Output written on /foo/output/errors.pdf (1 page, 28092 bytes). 248 | PDF statistics: 249 | 20 PDF objects out of 1000 (max. 8388607) 250 | 13 compressed objects within 1 object stream 251 | 0 named destinations out of 1000 (max. 500000) 252 | 1 words of extra memory for PDF output out of 10000 (max. 10000000) 253 | -------------------------------------------------------------------------------- /spec/fixtures/file.fdb_latexmk: -------------------------------------------------------------------------------- 1 | # Fdb version 3 2 | ["pdflatex"] 1474559881 "file.tex" "/foo/output/file.pdf" "file" 1474559881 3 | "/foo/output/file.aux" 1474559881 78 176b9809a80d53064f21009e02135ec4 "" 4 | "/usr/local/texlive/2016/texmf-dist/fonts/map/fontname/texfonts.map" 1272929888 3287 e6b82fe08f5336d4d5ebc73fb1152e87 "" 5 | "/usr/local/texlive/2016/texmf-dist/fonts/tfm/public/cm/cmbx10.tfm" 1136768653 1328 c834bbb027764024c09d3d2bf908b5f0 "" 6 | "/usr/local/texlive/2016/texmf-dist/fonts/tfm/public/cm/cmbx12.tfm" 1136768653 1324 c910af8c371558dc20f2d7822f66fe64 "" 7 | "/usr/local/texlive/2016/texmf-dist/fonts/tfm/public/cm/cmmi12.tfm" 1136768653 1524 4414a8315f39513458b80dfc63bff03a "" 8 | "/usr/local/texlive/2016/texmf-dist/fonts/tfm/public/cm/cmr12.tfm" 1136768653 1288 655e228510b4c2a1abe905c368440826 "" 9 | "/usr/local/texlive/2016/texmf-dist/fonts/tfm/public/cm/cmsy10.tfm" 1136768653 1124 6c73e740cf17375f03eec0ee63599741 "" 10 | "/usr/local/texlive/2016/texmf-dist/fonts/type1/public/amsfonts/cm/cmbx10.pfb" 1248133631 34811 78b52f49e893bcba91bd7581cdc144c0 "" 11 | "/usr/local/texlive/2016/texmf-dist/fonts/type1/public/amsfonts/cm/cmbx12.pfb" 1248133631 32080 340ef9bf63678554ee606688e7b5339d "" 12 | "/usr/local/texlive/2016/texmf-dist/fonts/type1/public/amsfonts/cm/cmr10.pfb" 1248133631 35752 024fb6c41858982481f6968b5fc26508 "" 13 | "/usr/local/texlive/2016/texmf-dist/tex/latex/base/article.cls" 1459635588 19821 310da678527a7dfe2a02c88af38079b7 "" 14 | "/usr/local/texlive/2016/texmf-dist/tex/latex/base/size10.clo" 1459635588 8292 e897c12e1e886ce77fe26afc5d470886 "" 15 | "/usr/local/texlive/2016/texmf-dist/web2c/texmf.cnf" 1470266149 31958 926ad82bc47ee89401cf24be7d2cbf7d "" 16 | "/usr/local/texlive/2016/texmf-var/fonts/map/pdftex/updmap/pdftex.map" 1471876454 1938064 4ded6634052b89be330bb7dac8cc40f0 "" 17 | "/usr/local/texlive/2016/texmf-var/web2c/pdftex/pdflatex.fmt" 1471547796 3873677 271c6d05ce0a4f4e34be832490419ac2 "" 18 | "/usr/local/texlive/2016/texmf.cnf" 1465311657 1020 2966fa5b7c5b86b293259f0ebdcea22d "" 19 | "file.tex" 1474559763 108 e2f7eacca0e6c4d22260c2d4b1150d46 "" 20 | "output/file.aux" 1474559881 78 176b9809a80d53064f21009e02135ec4 "" 21 | (generated) 22 | "/foo/output/file.pdfsync" 23 | "/foo/output/file.pdf" 24 | "output/file.log" 25 | "output/file.pdf" 26 | "/foo/output/file.log" 27 | "output/file.aux" 28 | -------------------------------------------------------------------------------- /spec/fixtures/file.log: -------------------------------------------------------------------------------- 1 | This is pdfTeX, Version 3.14159265-2.6-1.40.15 (TeX Live 2014) (preloaded format=pdflatex 2014.6.22) 6 JUL 2014 22:06 2 | entering extended mode 3 | \write18 enabled. 4 | file:line:error style messages enabled. 5 | %&-line parsing enabled. 6 | **file.tex 7 | (./file.tex 8 | LaTeX2e <2014/05/01> 9 | Babel <3.9k> and hyphenation patterns for 78 languages loaded. 10 | (/usr/local/texlive/2014/texmf-dist/tex/latex/base/article.cls 11 | Document Class: article 2007/10/19 v1.4h Standard LaTeX document class 12 | (/usr/local/texlive/2014/texmf-dist/tex/latex/base/size10.clo 13 | File: size10.clo 2007/10/19 v1.4h Standard LaTeX file (size option) 14 | ) 15 | \c@part=\count79 16 | \c@section=\count80 17 | \c@subsection=\count81 18 | \c@subsubsection=\count82 19 | \c@paragraph=\count83 20 | \c@subparagraph=\count84 21 | \c@figure=\count85 22 | \c@table=\count86 23 | \abovecaptionskip=\skip41 24 | \belowcaptionskip=\skip42 25 | \bibindent=\dimen102 26 | ) (/foo/output/file.aux) 27 | \openout1 = `file.aux'. 28 | 29 | LaTeX Font Info: Checking defaults for OML/cmm/m/it on input line 3. 30 | LaTeX Font Info: ... okay on input line 3. 31 | LaTeX Font Info: Checking defaults for T1/cmr/m/n on input line 3. 32 | LaTeX Font Info: ... okay on input line 3. 33 | LaTeX Font Info: Checking defaults for OT1/cmr/m/n on input line 3. 34 | LaTeX Font Info: ... okay on input line 3. 35 | LaTeX Font Info: Checking defaults for OMS/cmsy/m/n on input line 3. 36 | LaTeX Font Info: ... okay on input line 3. 37 | LaTeX Font Info: Checking defaults for OMX/cmex/m/n on input line 3. 38 | LaTeX Font Info: ... okay on input line 3. 39 | LaTeX Font Info: Checking defaults for U/cmr/m/n on input line 3. 40 | LaTeX Font Info: ... okay on input line 3. 41 | LaTeX Font Info: External font `cmex10' loaded for size 42 | (Font) <14.4> on input line 4. 43 | LaTeX Font Info: External font `cmex10' loaded for size 44 | (Font) <7> on input line 4. 45 | [1 46 | 47 | {/usr/local/texlive/2014/texmf-var/fonts/map/pdftex/updmap/pdftex.map}] (/foo/output/file.aux) ) 48 | Here is how much of TeX's memory you used: 49 | 213 strings out of 493117 50 | 2674 string characters out of 6135433 51 | 52909 words of memory out of 5000000 52 | 3758 multiletter control sequences out of 15000+600000 53 | 5168 words of font info for 19 fonts, out of 8000000 for 9000 54 | 1141 hyphenation exceptions out of 8191 55 | 23i,4n,20p,112b,117s stack positions out of 5000i,500n,10000p,200000b,80000s 56 | 57 | Output written on /foo/output/file.pdf (1 page, 28094 bytes). 58 | PDF statistics: 59 | 20 PDF objects out of 1000 (max. 8388607) 60 | 13 compressed objects within 1 object stream 61 | 0 named destinations out of 1000 (max. 500000) 62 | 1 words of extra memory for PDF output out of 10000 (max. 10000000) 63 | -------------------------------------------------------------------------------- /spec/fixtures/file.tex: -------------------------------------------------------------------------------- 1 | \documentclass{article} 2 | 3 | \begin{document} 4 | \section{\LaTeX} 5 | Lorem ipsum dolor sit amet... 6 | \end{document} 7 | -------------------------------------------------------------------------------- /spec/fixtures/filename with spaces.log: -------------------------------------------------------------------------------- 1 | This is pdfTeX, Version 3.14159265-2.6-1.40.15 (TeX Live 2014) (preloaded format=pdflatex 2014.6.22) 6 JUL 2014 22:06 2 | entering extended mode 3 | \write18 enabled. 4 | file:line:error style messages enabled. 5 | %&-line parsing enabled. 6 | **file.tex 7 | (./file.tex 8 | LaTeX2e <2014/05/01> 9 | Babel <3.9k> and hyphenation patterns for 78 languages loaded. 10 | (/usr/local/texlive/2014/texmf-dist/tex/latex/base/article.cls 11 | Document Class: article 2007/10/19 v1.4h Standard LaTeX document class 12 | (/usr/local/texlive/2014/texmf-dist/tex/latex/base/size10.clo 13 | File: size10.clo 2007/10/19 v1.4h Standard LaTeX file (size option) 14 | ) 15 | \c@part=\count79 16 | \c@section=\count80 17 | \c@subsection=\count81 18 | \c@subsubsection=\count82 19 | \c@paragraph=\count83 20 | \c@subparagraph=\count84 21 | \c@figure=\count85 22 | \c@table=\count86 23 | \abovecaptionskip=\skip41 24 | \belowcaptionskip=\skip42 25 | \bibindent=\dimen102 26 | ) (/foo/output/file.aux) 27 | \openout1 = `file.aux'. 28 | 29 | LaTeX Font Info: Checking defaults for OML/cmm/m/it on input line 3. 30 | LaTeX Font Info: ... okay on input line 3. 31 | LaTeX Font Info: Checking defaults for T1/cmr/m/n on input line 3. 32 | LaTeX Font Info: ... okay on input line 3. 33 | LaTeX Font Info: Checking defaults for OT1/cmr/m/n on input line 3. 34 | LaTeX Font Info: ... okay on input line 3. 35 | LaTeX Font Info: Checking defaults for OMS/cmsy/m/n on input line 3. 36 | LaTeX Font Info: ... okay on input line 3. 37 | LaTeX Font Info: Checking defaults for OMX/cmex/m/n on input line 3. 38 | LaTeX Font Info: ... okay on input line 3. 39 | LaTeX Font Info: Checking defaults for U/cmr/m/n on input line 3. 40 | LaTeX Font Info: ... okay on input line 3. 41 | LaTeX Font Info: External font `cmex10' loaded for size 42 | (Font) <14.4> on input line 4. 43 | LaTeX Font Info: External font `cmex10' loaded for size 44 | (Font) <7> on input line 4. 45 | [1 46 | 47 | {/usr/local/texlive/2014/texmf-var/fonts/map/pdftex/updmap/pdftex.map}] (/foo/output/filename with spaces.aux) ) 48 | Here is how much of TeX's memory you used: 49 | 213 strings out of 493117 50 | 2674 string characters out of 6135433 51 | 52909 words of memory out of 5000000 52 | 3758 multiletter control sequences out of 15000+600000 53 | 5168 words of font info for 19 fonts, out of 8000000 for 9000 54 | 1141 hyphenation exceptions out of 8191 55 | 23i,4n,20p,112b,117s stack positions out of 5000i,500n,10000p,200000b,80000s 56 | 57 | Output written on "/foo/output/filename with spaces.pdf" (1 page, 28094 bytes). 58 | PDF statistics: 59 | 20 PDF objects out of 1000 (max. 8388607) 60 | 13 compressed objects within 1 object stream 61 | 0 named destinations out of 1000 (max. 500000) 62 | 1 words of extra memory for PDF output out of 10000 (max. 10000000) 63 | -------------------------------------------------------------------------------- /spec/fixtures/filename with spaces.tex: -------------------------------------------------------------------------------- 1 | \documentclass{article} 2 | 3 | \begin{document} 4 | \section{\LaTeX} 5 | Lorem ipsum dolor sit amet... 6 | \end{document} 7 | -------------------------------------------------------------------------------- /spec/fixtures/knitr/file.Rnw: -------------------------------------------------------------------------------- 1 | \documentclass{article} 2 | 3 | \begin{document} 4 | \section{\LaTeX + knitr} 5 | <>= 6 | tau = 2 * pi 7 | @ 8 | 9 | $\tau \approx \Sexpr{round(tau, 10)}$ 10 | \end{document} 11 | -------------------------------------------------------------------------------- /spec/fixtures/latexmk/asymptote-test.tex: -------------------------------------------------------------------------------- 1 | \documentclass{article} 2 | 3 | \usepackage[inline]{asymptote} 4 | 5 | \begin{document} 6 | 7 | \begin{center} 8 | \begin{asy} 9 | import graph; 10 | size(300,IgnoreAspect); 11 | 12 | bool3 branch(real x) 13 | { 14 | static int lastsign=0; 15 | if(x <= 0 && x == floor(x)) return false; 16 | int sign=sgn(gamma(x)); 17 | bool b=lastsign == 0 || sign == lastsign; 18 | lastsign=sign; 19 | return b ? true : default; 20 | } 21 | 22 | draw(graph(gamma,-4,4,n=2000,branch),red); 23 | 24 | scale(false); 25 | xlimits(-4,4); 26 | ylimits(-6,6); 27 | crop(); 28 | 29 | xaxis("$x$",RightTicks(NoZero)); 30 | yaxis(LeftTicks(NoZero)); 31 | 32 | label("$\Gamma(x)$",(1,2),red); 33 | \end{asy} 34 | \end{center} 35 | 36 | \end{document} 37 | -------------------------------------------------------------------------------- /spec/fixtures/latexmk/glossaries-test.tex: -------------------------------------------------------------------------------- 1 | \documentclass{article} 2 | 3 | \usepackage[acronym]{glossaries} 4 | 5 | \makeglossaries 6 | 7 | \newglossaryentry{foo}{ 8 | name=foo, 9 | description={Metasyntactic variable used as a placeholder often in conjunction 10 | with bar} 11 | } 12 | 13 | \newglossaryentry{bar}{ 14 | name=bar, 15 | description={Metasyntactic variable used as a placeholder often in conjunction 16 | with foo} 17 | } 18 | 19 | \newacronym{gce}{GCE}{Gross Conceptual Error} 20 | \newacronym{rawq}{RAWQ}{Right Answer; Wrong Question} 21 | 22 | \begin{document} 23 | 24 | Without \gls{foo} there is no \gls{bar} which often leads to a \acrlong{gce}, 25 | which is abbreviated \acrshort{gce}. This is often preceded by \gls{rawq}. 26 | 27 | \printglossary[type=\acronymtype] 28 | \printglossary 29 | 30 | \end{document} -------------------------------------------------------------------------------- /spec/fixtures/latexmk/index-test.tex: -------------------------------------------------------------------------------- 1 | \documentclass{article} 2 | \usepackage{index} 3 | 4 | \makeindex 5 | \newindex[theenumi]{list}{ldx}{lnd}{Items} 6 | 7 | \begin{document} 8 | 9 | Wibble, wibble, foo\index{foo}, foo, bar\index{bar}. 10 | 11 | \begin{enumerate} 12 | \item Gronk 13 | \item Baz\index[list]{baz} 14 | \item Quux\index[list]{quux} 15 | \end{enumerate} 16 | 17 | \printindex[list] 18 | \printindex 19 | 20 | \end{document} 21 | -------------------------------------------------------------------------------- /spec/fixtures/latexmk/mpost-test.tex: -------------------------------------------------------------------------------- 1 | \documentclass{article} 2 | 3 | \usepackage[pdftex]{graphicx} 4 | \usepackage{feynmp} 5 | 6 | \DeclareGraphicsRule{.1}{mps}{*}{} 7 | 8 | \begin{document} 9 | 10 | \begin{fmffile}{mpost-test-feynmp} 11 | \begin{fmfgraph*}(120,80) 12 | \fmfpen{thin} 13 | \fmfleft{i1,i2} 14 | \fmfright{o1,o2} 15 | \fmf{fermion}{i1,v1,o1} 16 | \fmf{fermion}{i2,v2,o2} 17 | \fmf{photon,label=$\gamma$}{v1,v2} 18 | \fmfdot{v1,v2} 19 | \end{fmfgraph*} 20 | \end{fmffile} 21 | 22 | \end{document} 23 | -------------------------------------------------------------------------------- /spec/fixtures/latexmk/nomencl-test.tex: -------------------------------------------------------------------------------- 1 | \documentclass{article} 2 | 3 | \usepackage{nomencl} 4 | \makenomenclature 5 | 6 | \begin{document} 7 | 8 | \section{Wibble} 9 | 10 | \nomenclature{$G$}{Universal gravitational constant} 11 | \nomenclature{$\hbar$}{Dirac constant (reduced Planck constant)} 12 | 13 | \printnomenclature 14 | \end{document} -------------------------------------------------------------------------------- /spec/fixtures/latexmk/sagetex-test.tex: -------------------------------------------------------------------------------- 1 | \documentclass{article} 2 | \usepackage{sagetex} 3 | 4 | \begin{document} 5 | 6 | \begin{sageblock} 7 | f(x) = x^2 * exp(x) 8 | \end{sageblock} 9 | 10 | The second derivative of $f$ is 11 | 12 | \[ 13 | \frac{\mathrm{d}^{2}}{\mathrm{d}x^{2}} \left[ \sage{f(x)} \right] = 14 | \sage{diff(f, x, 2)(x)}. 15 | \] 16 | 17 | \end{document} 18 | -------------------------------------------------------------------------------- /spec/fixtures/log-parse/file-dvi.fdb_latexmk: -------------------------------------------------------------------------------- 1 | # Fdb version 3 2 | ["latex"] 1479339799 "file.tex" "log-parse/file-dvi.dvi" "file-dvi" 1479339799 3 | "file-dvi.aux" 0 -1 0 "" 4 | "file.tex" 1476733827 108 e2f7eacca0e6c4d22260c2d4b1150d46 "" 5 | (generated) 6 | "log-parse/file-dvi.log" 7 | "log-parse/file-dvi.dvi" 8 | "log-parse/file-dvi.aux" 9 | -------------------------------------------------------------------------------- /spec/fixtures/log-parse/file-dvi.log: -------------------------------------------------------------------------------- 1 | This is pdfTeX, Version 3.14159265-2.6-1.40.17 (TeX Live 2016) (preloaded format=latex 2016.11.6) 16 NOV 2016 18:43 2 | entering extended mode 3 | \write18 enabled. 4 | %&-line parsing enabled. 5 | **file.tex 6 | (./file.tex 7 | LaTeX2e <2016/03/31> patch level 3 8 | Babel <3.9r> and hyphenation patterns for 83 language(s) loaded. 9 | (/usr/local/texlive/2016/texmf-dist/tex/latex/base/article.cls 10 | Document Class: article 2014/09/29 v1.4h Standard LaTeX document class 11 | (/usr/local/texlive/2016/texmf-dist/tex/latex/base/size10.clo 12 | File: size10.clo 2014/09/29 v1.4h Standard LaTeX file (size option) 13 | ) 14 | \c@part=\count79 15 | \c@section=\count80 16 | \c@subsection=\count81 17 | \c@subsubsection=\count82 18 | \c@paragraph=\count83 19 | \c@subparagraph=\count84 20 | \c@figure=\count85 21 | \c@table=\count86 22 | \abovecaptionskip=\skip41 23 | \belowcaptionskip=\skip42 24 | \bibindent=\dimen102 25 | ) 26 | No file file-dvi.aux. 27 | \openout1 = `file-dvi.aux'. 28 | 29 | LaTeX Font Info: Checking defaults for OML/cmm/m/it on input line 3. 30 | LaTeX Font Info: ... okay on input line 3. 31 | LaTeX Font Info: Checking defaults for T1/cmr/m/n on input line 3. 32 | LaTeX Font Info: ... okay on input line 3. 33 | LaTeX Font Info: Checking defaults for OT1/cmr/m/n on input line 3. 34 | LaTeX Font Info: ... okay on input line 3. 35 | LaTeX Font Info: Checking defaults for OMS/cmsy/m/n on input line 3. 36 | LaTeX Font Info: ... okay on input line 3. 37 | LaTeX Font Info: Checking defaults for OMX/cmex/m/n on input line 3. 38 | LaTeX Font Info: ... okay on input line 3. 39 | LaTeX Font Info: Checking defaults for U/cmr/m/n on input line 3. 40 | LaTeX Font Info: ... okay on input line 3. 41 | LaTeX Font Info: External font `cmex10' loaded for size 42 | (Font) <14.4> on input line 4. 43 | LaTeX Font Info: External font `cmex10' loaded for size 44 | (Font) <7> on input line 4. 45 | [1 46 | 47 | ] (log-parse/file-dvi.aux) ) 48 | Here is how much of TeX's memory you used: 49 | 211 strings out of 493014 50 | 2323 string characters out of 6133369 51 | 53629 words of memory out of 5000000 52 | 3839 multiletter control sequences out of 15000+600000 53 | 5168 words of font info for 19 fonts, out of 8000000 for 9000 54 | 1141 hyphenation exceptions out of 8191 55 | 23i,4n,20p,112b,117s stack positions out of 5000i,500n,10000p,200000b,80000s 56 | 57 | Output written on log-parse/file-dvi.dvi (1 page, 388 bytes). 58 | -------------------------------------------------------------------------------- /spec/fixtures/log-parse/file-pdf.fdb_latexmk: -------------------------------------------------------------------------------- 1 | # Fdb version 3 2 | ["pdflatex"] 1479339798 "file.tex" "log-parse/file-pdf.pdf" "file-pdf" 1479339798 3 | "file-pdf.aux" 0 -1 0 "" 4 | "file.tex" 1476733827 108 e2f7eacca0e6c4d22260c2d4b1150d46 "" 5 | (generated) 6 | "log-parse/file-pdf.pdf" 7 | "log-parse/file-pdf.log" 8 | "log-parse/file-pdf.aux" 9 | -------------------------------------------------------------------------------- /spec/fixtures/log-parse/file-pdf.log: -------------------------------------------------------------------------------- 1 | This is pdfTeX, Version 3.14159265-2.6-1.40.17 (TeX Live 2016) (preloaded format=pdflatex 2016.11.6) 16 NOV 2016 18:43 2 | entering extended mode 3 | \write18 enabled. 4 | %&-line parsing enabled. 5 | **file.tex 6 | (./file.tex 7 | LaTeX2e <2016/03/31> patch level 3 8 | Babel <3.9r> and hyphenation patterns for 83 language(s) loaded. 9 | (/usr/local/texlive/2016/texmf-dist/tex/latex/base/article.cls 10 | Document Class: article 2014/09/29 v1.4h Standard LaTeX document class 11 | (/usr/local/texlive/2016/texmf-dist/tex/latex/base/size10.clo 12 | File: size10.clo 2014/09/29 v1.4h Standard LaTeX file (size option) 13 | ) 14 | \c@part=\count79 15 | \c@section=\count80 16 | \c@subsection=\count81 17 | \c@subsubsection=\count82 18 | \c@paragraph=\count83 19 | \c@subparagraph=\count84 20 | \c@figure=\count85 21 | \c@table=\count86 22 | \abovecaptionskip=\skip41 23 | \belowcaptionskip=\skip42 24 | \bibindent=\dimen102 25 | ) 26 | No file file-pdf.aux. 27 | \openout1 = `file-pdf.aux'. 28 | 29 | LaTeX Font Info: Checking defaults for OML/cmm/m/it on input line 3. 30 | LaTeX Font Info: ... okay on input line 3. 31 | LaTeX Font Info: Checking defaults for T1/cmr/m/n on input line 3. 32 | LaTeX Font Info: ... okay on input line 3. 33 | LaTeX Font Info: Checking defaults for OT1/cmr/m/n on input line 3. 34 | LaTeX Font Info: ... okay on input line 3. 35 | LaTeX Font Info: Checking defaults for OMS/cmsy/m/n on input line 3. 36 | LaTeX Font Info: ... okay on input line 3. 37 | LaTeX Font Info: Checking defaults for OMX/cmex/m/n on input line 3. 38 | LaTeX Font Info: ... okay on input line 3. 39 | LaTeX Font Info: Checking defaults for U/cmr/m/n on input line 3. 40 | LaTeX Font Info: ... okay on input line 3. 41 | LaTeX Font Info: External font `cmex10' loaded for size 42 | (Font) <14.4> on input line 4. 43 | LaTeX Font Info: External font `cmex10' loaded for size 44 | (Font) <7> on input line 4. 45 | [1 46 | 47 | {/usr/local/texlive/2016/texmf-var/fonts/map/pdftex/updmap/pdftex.map}] 48 | (log-parse/file-pdf.aux) ) 49 | Here is how much of TeX's memory you used: 50 | 212 strings out of 493013 51 | 2333 string characters out of 6133343 52 | 53632 words of memory out of 5000000 53 | 3839 multiletter control sequences out of 15000+600000 54 | 5168 words of font info for 19 fonts, out of 8000000 for 9000 55 | 1141 hyphenation exceptions out of 8191 56 | 23i,4n,20p,112b,117s stack positions out of 5000i,500n,10000p,200000b,80000s 57 | 61 | Output written on log-parse/file-pdf.pdf (1 page, 28091 bytes). 62 | PDF statistics: 63 | 20 PDF objects out of 1000 (max. 8388607) 64 | 13 compressed objects within 1 object stream 65 | 0 named destinations out of 1000 (max. 500000) 66 | 1 words of extra memory for PDF output out of 10000 (max. 10000000) 67 | 68 | -------------------------------------------------------------------------------- /spec/fixtures/log-parse/file-pdfdvi.fdb_latexmk: -------------------------------------------------------------------------------- 1 | # Fdb version 3 2 | ["dvipdf"] 1479339798 "log-parse/file-pdfdvi.dvi" "log-parse/file-pdfdvi.pdf" "log-parse/file-pdfdvi" 1479339799 3 | "log-parse/file-pdfdvi.dvi" 1479339798 388 f41d63987574afa39860a8b64bdc7c18 "latex" 4 | (generated) 5 | "log-parse/file-pdfdvi.pdf" 6 | ["latex"] 1479339798 "file.tex" "log-parse/file-pdfdvi.dvi" "file-pdfdvi" 1479339799 7 | "file-pdfdvi.aux" 0 -1 0 "" 8 | "file.tex" 1476733827 108 e2f7eacca0e6c4d22260c2d4b1150d46 "" 9 | (generated) 10 | "log-parse/file-pdfdvi.dvi" 11 | "log-parse/file-pdfdvi.log" 12 | "log-parse/file-pdfdvi.aux" 13 | -------------------------------------------------------------------------------- /spec/fixtures/log-parse/file-pdfdvi.log: -------------------------------------------------------------------------------- 1 | This is pdfTeX, Version 3.14159265-2.6-1.40.17 (TeX Live 2016) (preloaded format=latex 2016.11.6) 16 NOV 2016 18:43 2 | entering extended mode 3 | \write18 enabled. 4 | %&-line parsing enabled. 5 | **file.tex 6 | (./file.tex 7 | LaTeX2e <2016/03/31> patch level 3 8 | Babel <3.9r> and hyphenation patterns for 83 language(s) loaded. 9 | (/usr/local/texlive/2016/texmf-dist/tex/latex/base/article.cls 10 | Document Class: article 2014/09/29 v1.4h Standard LaTeX document class 11 | (/usr/local/texlive/2016/texmf-dist/tex/latex/base/size10.clo 12 | File: size10.clo 2014/09/29 v1.4h Standard LaTeX file (size option) 13 | ) 14 | \c@part=\count79 15 | \c@section=\count80 16 | \c@subsection=\count81 17 | \c@subsubsection=\count82 18 | \c@paragraph=\count83 19 | \c@subparagraph=\count84 20 | \c@figure=\count85 21 | \c@table=\count86 22 | \abovecaptionskip=\skip41 23 | \belowcaptionskip=\skip42 24 | \bibindent=\dimen102 25 | ) 26 | No file file-pdfdvi.aux. 27 | \openout1 = `file-pdfdvi.aux'. 28 | 29 | LaTeX Font Info: Checking defaults for OML/cmm/m/it on input line 3. 30 | LaTeX Font Info: ... okay on input line 3. 31 | LaTeX Font Info: Checking defaults for T1/cmr/m/n on input line 3. 32 | LaTeX Font Info: ... okay on input line 3. 33 | LaTeX Font Info: Checking defaults for OT1/cmr/m/n on input line 3. 34 | LaTeX Font Info: ... okay on input line 3. 35 | LaTeX Font Info: Checking defaults for OMS/cmsy/m/n on input line 3. 36 | LaTeX Font Info: ... okay on input line 3. 37 | LaTeX Font Info: Checking defaults for OMX/cmex/m/n on input line 3. 38 | LaTeX Font Info: ... okay on input line 3. 39 | LaTeX Font Info: Checking defaults for U/cmr/m/n on input line 3. 40 | LaTeX Font Info: ... okay on input line 3. 41 | LaTeX Font Info: External font `cmex10' loaded for size 42 | (Font) <14.4> on input line 4. 43 | LaTeX Font Info: External font `cmex10' loaded for size 44 | (Font) <7> on input line 4. 45 | [1 46 | 47 | ] (log-parse/file-pdfdvi.aux) ) 48 | Here is how much of TeX's memory you used: 49 | 211 strings out of 493014 50 | 2338 string characters out of 6133369 51 | 53629 words of memory out of 5000000 52 | 3839 multiletter control sequences out of 15000+600000 53 | 5168 words of font info for 19 fonts, out of 8000000 for 9000 54 | 1141 hyphenation exceptions out of 8191 55 | 23i,4n,20p,112b,117s stack positions out of 5000i,500n,10000p,200000b,80000s 56 | 57 | Output written on log-parse/file-pdfdvi.dvi (1 page, 388 bytes). 58 | -------------------------------------------------------------------------------- /spec/fixtures/log-parse/file-pdfps.fdb_latexmk: -------------------------------------------------------------------------------- 1 | # Fdb version 3 2 | ["dvips"] 1479339798 "log-parse/file-pdfps.dvi" "log-parse/file-pdfps.ps" "log-parse/file-pdfps" 1479339798 3 | "log-parse/file-pdfps.dvi" 1479339798 388 f41d63987574afa39860a8b64bdc7c18 "latex" 4 | (generated) 5 | "log-parse/file-pdfps.ps" 6 | ["latex"] 1479339798 "file.tex" "log-parse/file-pdfps.dvi" "file-pdfps" 1479339798 7 | "file-pdfps.aux" 0 -1 0 "" 8 | "file.tex" 1476733827 108 e2f7eacca0e6c4d22260c2d4b1150d46 "" 9 | (generated) 10 | "log-parse/file-pdfps.aux" 11 | "log-parse/file-pdfps.log" 12 | "log-parse/file-pdfps.dvi" 13 | ["ps2pdf"] 1479339798 "log-parse/file-pdfps.ps" "log-parse/file-pdfps.pdf" "log-parse/file-pdfps" 1479339798 14 | "log-parse/file-pdfps.ps" 1479339798 58953 def11a7aa0ae54e8f8c1eff9332043a0 "dvips" 15 | (generated) 16 | "log-parse/file-pdfps.pdf" 17 | -------------------------------------------------------------------------------- /spec/fixtures/log-parse/file-pdfps.log: -------------------------------------------------------------------------------- 1 | This is pdfTeX, Version 3.14159265-2.6-1.40.17 (TeX Live 2016) (preloaded format=latex 2016.11.6) 16 NOV 2016 18:43 2 | entering extended mode 3 | \write18 enabled. 4 | %&-line parsing enabled. 5 | **file.tex 6 | (./file.tex 7 | LaTeX2e <2016/03/31> patch level 3 8 | Babel <3.9r> and hyphenation patterns for 83 language(s) loaded. 9 | (/usr/local/texlive/2016/texmf-dist/tex/latex/base/article.cls 10 | Document Class: article 2014/09/29 v1.4h Standard LaTeX document class 11 | (/usr/local/texlive/2016/texmf-dist/tex/latex/base/size10.clo 12 | File: size10.clo 2014/09/29 v1.4h Standard LaTeX file (size option) 13 | ) 14 | \c@part=\count79 15 | \c@section=\count80 16 | \c@subsection=\count81 17 | \c@subsubsection=\count82 18 | \c@paragraph=\count83 19 | \c@subparagraph=\count84 20 | \c@figure=\count85 21 | \c@table=\count86 22 | \abovecaptionskip=\skip41 23 | \belowcaptionskip=\skip42 24 | \bibindent=\dimen102 25 | ) 26 | No file file-pdfps.aux. 27 | \openout1 = `file-pdfps.aux'. 28 | 29 | LaTeX Font Info: Checking defaults for OML/cmm/m/it on input line 3. 30 | LaTeX Font Info: ... okay on input line 3. 31 | LaTeX Font Info: Checking defaults for T1/cmr/m/n on input line 3. 32 | LaTeX Font Info: ... okay on input line 3. 33 | LaTeX Font Info: Checking defaults for OT1/cmr/m/n on input line 3. 34 | LaTeX Font Info: ... okay on input line 3. 35 | LaTeX Font Info: Checking defaults for OMS/cmsy/m/n on input line 3. 36 | LaTeX Font Info: ... okay on input line 3. 37 | LaTeX Font Info: Checking defaults for OMX/cmex/m/n on input line 3. 38 | LaTeX Font Info: ... okay on input line 3. 39 | LaTeX Font Info: Checking defaults for U/cmr/m/n on input line 3. 40 | LaTeX Font Info: ... okay on input line 3. 41 | LaTeX Font Info: External font `cmex10' loaded for size 42 | (Font) <14.4> on input line 4. 43 | LaTeX Font Info: External font `cmex10' loaded for size 44 | (Font) <7> on input line 4. 45 | [1 46 | 47 | ] (log-parse/file-pdfps.aux) ) 48 | Here is how much of TeX's memory you used: 49 | 211 strings out of 493014 50 | 2333 string characters out of 6133369 51 | 53629 words of memory out of 5000000 52 | 3839 multiletter control sequences out of 15000+600000 53 | 5168 words of font info for 19 fonts, out of 8000000 for 9000 54 | 1141 hyphenation exceptions out of 8191 55 | 23i,4n,20p,112b,117s stack positions out of 5000i,500n,10000p,200000b,80000s 56 | 57 | Output written on log-parse/file-pdfps.dvi (1 page, 388 bytes). 58 | -------------------------------------------------------------------------------- /spec/fixtures/log-parse/file-ps.fdb_latexmk: -------------------------------------------------------------------------------- 1 | # Fdb version 3 2 | ["dvips"] 1479339799 "log-parse/file-ps.dvi" "log-parse/file-ps.ps" "log-parse/file-ps" 1479339799 3 | "log-parse/file-ps.dvi" 1479339799 388 f41d63987574afa39860a8b64bdc7c18 "latex" 4 | (generated) 5 | "log-parse/file-ps.ps" 6 | ["latex"] 1479339799 "file.tex" "log-parse/file-ps.dvi" "file-ps" 1479339799 7 | "file-ps.aux" 0 -1 0 "" 8 | "file.tex" 1476733827 108 e2f7eacca0e6c4d22260c2d4b1150d46 "" 9 | (generated) 10 | "log-parse/file-ps.aux" 11 | "log-parse/file-ps.log" 12 | "log-parse/file-ps.dvi" 13 | -------------------------------------------------------------------------------- /spec/fixtures/log-parse/file-ps.log: -------------------------------------------------------------------------------- 1 | This is pdfTeX, Version 3.14159265-2.6-1.40.17 (TeX Live 2016) (preloaded format=latex 2016.11.6) 16 NOV 2016 18:43 2 | entering extended mode 3 | \write18 enabled. 4 | %&-line parsing enabled. 5 | **file.tex 6 | (./file.tex 7 | LaTeX2e <2016/03/31> patch level 3 8 | Babel <3.9r> and hyphenation patterns for 83 language(s) loaded. 9 | (/usr/local/texlive/2016/texmf-dist/tex/latex/base/article.cls 10 | Document Class: article 2014/09/29 v1.4h Standard LaTeX document class 11 | (/usr/local/texlive/2016/texmf-dist/tex/latex/base/size10.clo 12 | File: size10.clo 2014/09/29 v1.4h Standard LaTeX file (size option) 13 | ) 14 | \c@part=\count79 15 | \c@section=\count80 16 | \c@subsection=\count81 17 | \c@subsubsection=\count82 18 | \c@paragraph=\count83 19 | \c@subparagraph=\count84 20 | \c@figure=\count85 21 | \c@table=\count86 22 | \abovecaptionskip=\skip41 23 | \belowcaptionskip=\skip42 24 | \bibindent=\dimen102 25 | ) 26 | No file file-ps.aux. 27 | \openout1 = `file-ps.aux'. 28 | 29 | LaTeX Font Info: Checking defaults for OML/cmm/m/it on input line 3. 30 | LaTeX Font Info: ... okay on input line 3. 31 | LaTeX Font Info: Checking defaults for T1/cmr/m/n on input line 3. 32 | LaTeX Font Info: ... okay on input line 3. 33 | LaTeX Font Info: Checking defaults for OT1/cmr/m/n on input line 3. 34 | LaTeX Font Info: ... okay on input line 3. 35 | LaTeX Font Info: Checking defaults for OMS/cmsy/m/n on input line 3. 36 | LaTeX Font Info: ... okay on input line 3. 37 | LaTeX Font Info: Checking defaults for OMX/cmex/m/n on input line 3. 38 | LaTeX Font Info: ... okay on input line 3. 39 | LaTeX Font Info: Checking defaults for U/cmr/m/n on input line 3. 40 | LaTeX Font Info: ... okay on input line 3. 41 | LaTeX Font Info: External font `cmex10' loaded for size 42 | (Font) <14.4> on input line 4. 43 | LaTeX Font Info: External font `cmex10' loaded for size 44 | (Font) <7> on input line 4. 45 | [1 46 | 47 | ] (log-parse/file-ps.aux) ) 48 | Here is how much of TeX's memory you used: 49 | 211 strings out of 493014 50 | 2318 string characters out of 6133369 51 | 53629 words of memory out of 5000000 52 | 3839 multiletter control sequences out of 15000+600000 53 | 5168 words of font info for 19 fonts, out of 8000000 for 9000 54 | 1141 hyphenation exceptions out of 8191 55 | 23i,4n,20p,112b,117s stack positions out of 5000i,500n,10000p,200000b,80000s 56 | 57 | Output written on log-parse/file-ps.dvi (1 page, 388 bytes). 58 | -------------------------------------------------------------------------------- /spec/fixtures/magic-comments/multiple-magic-comments.tex: -------------------------------------------------------------------------------- 1 | % !TEX root = ../file.tex 2 | % !TEX program = lualatex 3 | \documentclass{article} 4 | 5 | \begin{document} 6 | \section{\LaTeX} 7 | Lorem ipsum dolor sit amet... 8 | \end{document} 9 | -------------------------------------------------------------------------------- /spec/fixtures/magic-comments/no-whitespace.tex: -------------------------------------------------------------------------------- 1 | %!TEX root=../file.tex 2 | \documentclass{article} 3 | 4 | \begin{document} 5 | \section{\LaTeX} 6 | Lorem ipsum dolor sit amet... 7 | \end{document} 8 | -------------------------------------------------------------------------------- /spec/fixtures/magic-comments/not-first-line.tex: -------------------------------------------------------------------------------- 1 | % foo 2 | % !TEX root = ../file.tex 3 | \documentclass{article} 4 | 5 | \begin{document} 6 | \section{\LaTeX} 7 | Lorem ipsum dolor sit amet... 8 | \end{document} 9 | -------------------------------------------------------------------------------- /spec/fixtures/magic-comments/override-settings.tex: -------------------------------------------------------------------------------- 1 | % !TEX program = lualatex 2 | % !TEX format = ps 3 | % !TEX jobnames = foo bar, snafu 4 | % !TEX producer = xdvipdfmx 5 | % !TEX output_directory = wibble 6 | % !TEX cleanPatterns = **/*.quux, foo/bar 7 | % !TEX enableShellEscape = yes 8 | % !TEX enableSynctex = true 9 | % !TEX enableExtendedBuildMode = YES 10 | % !TEX moveResultToSourceDirectory = Yes 11 | \documentclass{article} 12 | 13 | \begin{document} 14 | \section{\LaTeX} 15 | Lorem ipsum dolor sit amet... 16 | \end{document} 17 | -------------------------------------------------------------------------------- /spec/fixtures/magic-comments/override-settings.yaml: -------------------------------------------------------------------------------- 1 | engine: xelatex 2 | format: dvi 3 | jobNames: 4 | - wibble 5 | - quux 6 | producer: ps2pdf 7 | outputDirectory: foo 8 | cleanPatterns: 9 | - '**/*.snafu' 10 | - foo/bar/bax 11 | enableShellEscape: yes 12 | enableSynctex: true 13 | enableExtendedBuildMode: YES 14 | moveResultToSourceDirectory: Yes 15 | -------------------------------------------------------------------------------- /spec/fixtures/magic-comments/root-comment.tex: -------------------------------------------------------------------------------- 1 | % !TEX root = ../file.tex 2 | \documentclass{article} 3 | 4 | \begin{document} 5 | \section{\LaTeX} 6 | Lorem ipsum dolor sit amet... 7 | \end{document} 8 | -------------------------------------------------------------------------------- /spec/fixtures/master-tex-finder/multiple-masters/inc1.tex: -------------------------------------------------------------------------------- 1 | \section{inc1} 2 | -------------------------------------------------------------------------------- /spec/fixtures/master-tex-finder/multiple-masters/inc2.tex: -------------------------------------------------------------------------------- 1 | \section{inc2} 2 | -------------------------------------------------------------------------------- /spec/fixtures/master-tex-finder/multiple-masters/master1.tex: -------------------------------------------------------------------------------- 1 | \documentclass[a4paper]{article} 2 | 3 | 4 | \begin{document} 5 | 6 | \include{inc1.tex} 7 | \end{document} 8 | -------------------------------------------------------------------------------- /spec/fixtures/master-tex-finder/multiple-masters/master2.tex: -------------------------------------------------------------------------------- 1 | \documentclass{article} 2 | 3 | 4 | \begin{document} 5 | 6 | \include{inc2.tex} 7 | \end{document} 8 | -------------------------------------------------------------------------------- /spec/fixtures/master-tex-finder/single-master/inc1.tex: -------------------------------------------------------------------------------- 1 | % !TEX root = master.tex 2 | 3 | \section{Inc1} 4 | Inc1 5 | -------------------------------------------------------------------------------- /spec/fixtures/master-tex-finder/single-master/inc2.tex: -------------------------------------------------------------------------------- 1 | % inc2 2 | 3 | \section{Inc2} 4 | Inc2 5 | 6 | \include{inc3} 7 | -------------------------------------------------------------------------------- /spec/fixtures/master-tex-finder/single-master/inc3.tex: -------------------------------------------------------------------------------- 1 | % inc3 2 | 3 | \section{Inc3} 4 | Inc3 5 | -------------------------------------------------------------------------------- /spec/fixtures/master-tex-finder/single-master/master.log: -------------------------------------------------------------------------------- 1 | Master log 2 | -------------------------------------------------------------------------------- /spec/fixtures/master-tex-finder/single-master/master.tex: -------------------------------------------------------------------------------- 1 | \documentclass{article} 2 | 3 | \begin{document} 4 | \include{inc1.tex} 5 | \include{inc2.tex} 6 | \input{inc3.tex} 7 | \end{document} 8 | -------------------------------------------------------------------------------- /spec/fixtures/sub/foo bar.tex: -------------------------------------------------------------------------------- 1 | % Class errors, warnings, etc. 2 | \ClassError{foo}{Significant class issue}{RTFM!} 3 | \ClassWarning{foo}{Class issue} 4 | \ClassWarningNoLine{foo}{Nebulous class issue} 5 | \ClassInfo{foo}{Insignificant class issue} 6 | -------------------------------------------------------------------------------- /spec/fixtures/sub/wibble.tex: -------------------------------------------------------------------------------- 1 | % Package errors, warnings, etc. 2 | \PackageError{bar}{Significant package issue}{RTFM!} 3 | \PackageWarning{bar}{Package issue} 4 | \PackageWarningNoLine{bar}{Nebulous package issue} 5 | \PackageInfo{bar}{Insignificant package issue} 6 | 7 | % Undefined references 8 | \ref{tab:snafu} 9 | -------------------------------------------------------------------------------- /spec/latex-spec.js: -------------------------------------------------------------------------------- 1 | /** @babel */ 2 | 3 | // eslint-disable-next-line no-unused-vars 4 | import { afterEach, beforeEach, it, fit } from './async-spec-helpers' 5 | import { activatePackages } from './spec-helpers' 6 | 7 | describe('Latex', () => { 8 | beforeEach(async () => { 9 | await activatePackages() 10 | }) 11 | 12 | describe('initialize', () => { 13 | it('initializes all properties', () => { 14 | expect(latex.log).toBeDefined() 15 | expect(latex.opener).toBeDefined() 16 | expect(latex.process).toBeDefined() 17 | }) 18 | }) 19 | }) 20 | -------------------------------------------------------------------------------- /spec/logger-spec.js: -------------------------------------------------------------------------------- 1 | /** @babel */ 2 | 3 | // eslint-disable-next-line no-unused-vars 4 | import { afterEach, beforeEach, it, fit } from './async-spec-helpers' 5 | 6 | import Logger from '../lib/logger' 7 | import werkzeug from '../lib/werkzeug' 8 | 9 | describe('Logger', () => { 10 | let logger, messagesListener 11 | 12 | function initialize (loggingLevel = 'warning') { 13 | logger = new Logger() 14 | messagesListener = jasmine.createSpy('onMessagesListener') 15 | logger.onMessages(messagesListener) 16 | 17 | atom.config.set('latex.loggingLevel', loggingLevel) 18 | 19 | logger.info() 20 | logger.warning() 21 | logger.error() 22 | } 23 | 24 | describe('getMessages', () => { 25 | it('verifies no messages filtered when logging level set to info', () => { 26 | initialize('info') 27 | 28 | expect(logger.getMessages()).toEqual([ 29 | { type: 'info' }, 30 | { type: 'warning' }, 31 | { type: 'error' } 32 | ]) 33 | }) 34 | 35 | it('verifies info messages filtered when logging level set to warning', () => { 36 | initialize('warning') 37 | 38 | expect(logger.getMessages()).toEqual([ 39 | { type: 'warning' }, 40 | { type: 'error' } 41 | ]) 42 | }) 43 | 44 | it('verifies warning and info messages filtered when logging level set to error', () => { 45 | initialize('error') 46 | 47 | expect(logger.getMessages()).toEqual([ 48 | { type: 'error' } 49 | ]) 50 | }) 51 | 52 | it('verifies no messages filtered when useFilters is false', () => { 53 | initialize('error') 54 | 55 | expect(logger.getMessages(false)).toEqual([ 56 | { type: 'info' }, 57 | { type: 'warning' }, 58 | { type: 'error' } 59 | ]) 60 | }) 61 | }) 62 | 63 | describe('messageTypeIsVisible', () => { 64 | it('verifies messageTypeIsVisible is true for all levels when logging level set to info', () => { 65 | initialize('info') 66 | 67 | expect(logger.messageTypeIsVisible('info')).toBe(true) 68 | expect(logger.messageTypeIsVisible('warning')).toBe(true) 69 | expect(logger.messageTypeIsVisible('error')).toBe(true) 70 | }) 71 | 72 | it('verifies messageTypeIsVisible is false for info when logging level set to warning', () => { 73 | initialize('warning') 74 | 75 | expect(logger.messageTypeIsVisible('info')).toBe(false) 76 | expect(logger.messageTypeIsVisible('warning')).toBe(true) 77 | expect(logger.messageTypeIsVisible('error')).toBe(true) 78 | }) 79 | 80 | it('verifies messageTypeIsVisible is false for info when logging level set to warning', () => { 81 | initialize('error') 82 | 83 | expect(logger.messageTypeIsVisible('info')).toBe(false) 84 | expect(logger.messageTypeIsVisible('warning')).toBe(false) 85 | expect(logger.messageTypeIsVisible('error')).toBe(true) 86 | }) 87 | }) 88 | 89 | describe('sync', () => { 90 | let logDock 91 | 92 | function initializeSpies (filePath, position) { 93 | initialize() 94 | 95 | logDock = jasmine.createSpyObj('LogDock', ['update']) 96 | spyOn(logger, 'show').andCallFake(() => Promise.resolve(logDock)) 97 | spyOn(werkzeug, 'getEditorDetails').andReturn({ filePath, position }) 98 | } 99 | 100 | it('silently does nothing when the current editor is transient', async () => { 101 | initializeSpies(null, null) 102 | 103 | await logger.sync() 104 | 105 | expect(logger.show).not.toHaveBeenCalled() 106 | expect(logDock.update).not.toHaveBeenCalled() 107 | }) 108 | 109 | it('shows and updates the log panel with the file path and position', async () => { 110 | const filePath = 'file.tex' 111 | const position = [[0, 0], [0, 10]] 112 | 113 | initializeSpies(filePath, position) 114 | 115 | await logger.sync() 116 | 117 | expect(logger.show).toHaveBeenCalled() 118 | expect(logDock.update).toHaveBeenCalledWith({ filePath, position }) 119 | }) 120 | }) 121 | 122 | describe('onMessages', () => { 123 | it('verifies no messages filtered when logging level set to info', () => { 124 | initialize('info') 125 | 126 | expect(messagesListener).toHaveBeenCalledWith({ 127 | messages: [{ type: 'info' }], 128 | reset: false 129 | }) 130 | expect(messagesListener).toHaveBeenCalledWith({ 131 | messages: [{ type: 'warning' }], 132 | reset: false 133 | }) 134 | expect(messagesListener).toHaveBeenCalledWith({ 135 | messages: [{ type: 'error' }], 136 | reset: false 137 | }) 138 | }) 139 | 140 | it('verifies info messages filtered when logging level set to warning', () => { 141 | initialize('warning') 142 | 143 | expect(messagesListener).not.toHaveBeenCalledWith({ 144 | messages: [{ type: 'info' }], 145 | reset: false 146 | }) 147 | expect(messagesListener).toHaveBeenCalledWith({ 148 | messages: [{ type: 'warning' }], 149 | reset: false 150 | }) 151 | expect(messagesListener).toHaveBeenCalledWith({ 152 | messages: [{ type: 'error' }], 153 | reset: false 154 | }) 155 | }) 156 | 157 | it('verifies warning and info messages filtered when logging level set to error', () => { 158 | initialize('error') 159 | 160 | expect(messagesListener).not.toHaveBeenCalledWith({ 161 | messages: [{ type: 'info' }], 162 | reset: false 163 | }) 164 | expect(messagesListener).not.toHaveBeenCalledWith({ 165 | messages: [{ type: 'warning' }], 166 | reset: false 167 | }) 168 | expect(messagesListener).toHaveBeenCalledWith({ 169 | messages: [{ type: 'error' }], 170 | reset: false 171 | }) 172 | }) 173 | 174 | it('verifies a new message list is sent when the logging level is changed', () => { 175 | initialize('info') 176 | 177 | atom.config.set('latex.loggingLevel', 'error') 178 | 179 | expect(messagesListener).toHaveBeenCalledWith({ 180 | messages: [{ type: 'error' }], 181 | reset: true 182 | }) 183 | }) 184 | }) 185 | 186 | describe('setMessages', () => { 187 | it('replaces message list and sends reset signal when called', () => { 188 | const messages = [{ type: 'error', text: 'foo' }] 189 | 190 | initialize('info') 191 | logger.setMessages(messages) 192 | 193 | expect(messagesListener).toHaveBeenCalledWith({ messages, reset: true }) 194 | expect(logger.getMessages(false)).toEqual(messages) 195 | }) 196 | }) 197 | 198 | describe('clear', () => { 199 | it('empties message list and sends reset signal when called', () => { 200 | initialize('info') 201 | logger.clear() 202 | 203 | expect(messagesListener).toHaveBeenCalledWith({ messages: [], reset: true }) 204 | expect(logger.getMessages(false)).toEqual([]) 205 | }) 206 | }) 207 | 208 | describe('refresh', () => { 209 | it('sends reset signal when called', () => { 210 | const messages = [{ type: 'error' }] 211 | 212 | initialize('error') 213 | logger.refresh() 214 | 215 | expect(messagesListener).toHaveBeenCalledWith({ messages, reset: true }) 216 | }) 217 | }) 218 | }) 219 | -------------------------------------------------------------------------------- /spec/marker-manager-spec.js: -------------------------------------------------------------------------------- 1 | /** @babel */ 2 | 3 | // eslint-disable-next-line no-unused-vars 4 | import { afterEach, beforeEach, it, fit } from './async-spec-helpers' 5 | import { activatePackages } from './spec-helpers' 6 | 7 | import MarkerManager from '../lib/marker-manager' 8 | 9 | describe('MarkerManager', () => { 10 | beforeEach(async () => { 11 | await activatePackages() 12 | }) 13 | 14 | describe('addMarkers', () => { 15 | let manager 16 | 17 | beforeEach(() => { 18 | const editor = { 19 | getPath: () => 'foo.tex', 20 | onDidDestroy: () => ({ dispose: () => {} }) 21 | } 22 | manager = new MarkerManager(editor) 23 | spyOn(manager, 'addMarker') 24 | spyOn(manager, 'clear') 25 | }) 26 | 27 | it('verifies that only messages that have a range and a matching file path are marked', () => { 28 | const messages = [ 29 | { type: 'error', range: [[0, 0], [0, 1]], filePath: 'foo.tex' }, 30 | { type: 'warning', range: [[0, 0], [0, 1]], filePath: 'bar.tex' }, 31 | { type: 'info', filePath: 'foo.tex' } 32 | ] 33 | manager.addMarkers(messages, false) 34 | 35 | expect(manager.addMarker).toHaveBeenCalledWith('error', 'foo.tex', [[0, 0], [0, 1]]) 36 | expect(manager.addMarker.calls.length).toEqual(1) 37 | expect(manager.clear).not.toHaveBeenCalled() 38 | }) 39 | 40 | it('verifies that clear is called when reset flag is set', () => { 41 | manager.addMarkers([], true) 42 | 43 | expect(manager.clear).toHaveBeenCalled() 44 | }) 45 | }) 46 | }) 47 | -------------------------------------------------------------------------------- /spec/opener-registry-spec.js: -------------------------------------------------------------------------------- 1 | /** @babel */ 2 | 3 | // eslint-disable-next-line no-unused-vars 4 | import { afterEach, beforeEach, it, fit } from './async-spec-helpers' 5 | import { activatePackages } from './spec-helpers' 6 | 7 | describe('OpenerRegistry', () => { 8 | const filePath = 'wibble.pdf' 9 | // The various viewers 10 | let cannotOpen, canOpen, canOpenInBackground, canOpenWithSynctex 11 | 12 | beforeEach(async () => { 13 | await activatePackages() 14 | }) 15 | 16 | function createOpener (name, canOpen, hasSynctex, canOpenInBackground) { 17 | const opener = jasmine.createSpyObj(name, [ 18 | 'open', 19 | 'canOpen', 20 | 'hasSynctex', 21 | 'canOpenInBackground' 22 | ]) 23 | 24 | opener.open.andCallFake(() => Promise.resolve()) 25 | opener.canOpen.andReturn(canOpen) 26 | opener.hasSynctex.andReturn(hasSynctex) 27 | opener.canOpenInBackground.andReturn(canOpenInBackground) 28 | 29 | latex.opener.openers.set(name, opener) 30 | 31 | return opener 32 | } 33 | 34 | beforeEach(() => { 35 | latex.opener.openers.clear() 36 | 37 | // The opener names have to conform to latex.opener schema 38 | cannotOpen = createOpener('skim', false, true, true) 39 | canOpen = createOpener('xdg-open', true, false, false) 40 | canOpenInBackground = createOpener('okular', true, false, true) 41 | canOpenWithSynctex = createOpener('evince', true, true, false) 42 | }) 43 | 44 | describe('open', () => { 45 | it('opens using preferred viewer even if it does not have requested features', async () => { 46 | atom.config.set('latex.enableSynctex', true) 47 | atom.config.set('latex.openResultInBackground', true) 48 | atom.config.set('latex.opener', 'xdg-open') 49 | 50 | await latex.opener.open(filePath) 51 | 52 | expect(cannotOpen.open).not.toHaveBeenCalled() 53 | expect(canOpen.open).toHaveBeenCalled() 54 | expect(canOpenInBackground.open).not.toHaveBeenCalled() 55 | expect(canOpenWithSynctex.open).not.toHaveBeenCalled() 56 | }) 57 | 58 | it('opens viewer that supports SyncTeX when enabled', async () => { 59 | atom.config.set('latex.enableSynctex', true) 60 | atom.config.set('latex.openResultInBackground', true) 61 | atom.config.set('latex.opener', 'automatic') 62 | 63 | await latex.opener.open(filePath) 64 | 65 | expect(cannotOpen.open).not.toHaveBeenCalled() 66 | expect(canOpen.open).not.toHaveBeenCalled() 67 | expect(canOpenInBackground.open).not.toHaveBeenCalled() 68 | expect(canOpenWithSynctex.open).toHaveBeenCalled() 69 | }) 70 | 71 | it('opens viewer that supports background opening when enabled', async () => { 72 | atom.config.set('latex.enableSynctex', false) 73 | atom.config.set('latex.openResultInBackground', true) 74 | atom.config.set('latex.opener', 'automatic') 75 | 76 | await latex.opener.open(filePath) 77 | 78 | expect(cannotOpen.open).not.toHaveBeenCalled() 79 | expect(canOpen.open).not.toHaveBeenCalled() 80 | expect(canOpenInBackground.open).toHaveBeenCalled() 81 | expect(canOpenWithSynctex.open).not.toHaveBeenCalled() 82 | }) 83 | }) 84 | }) 85 | -------------------------------------------------------------------------------- /spec/parsers/fdb-parser-spec.js: -------------------------------------------------------------------------------- 1 | /** @babel */ 2 | 3 | // eslint-disable-next-line no-unused-vars 4 | import { afterEach, beforeEach, it, fit } from '../async-spec-helpers' 5 | 6 | import path from 'path' 7 | import FdbParser from '../../lib/parsers/fdb-parser' 8 | 9 | describe('FdbParser', () => { 10 | let fixturesPath, fdbFile, texFile 11 | 12 | beforeEach(() => { 13 | fixturesPath = atom.project.getPaths()[0] 14 | fdbFile = path.join(fixturesPath, 'log-parse', 'file-pdfps.fdb_latexmk') 15 | texFile = path.join(fixturesPath, 'file.tex') 16 | }) 17 | 18 | describe('parse', () => { 19 | it('returns the expected parsed fdb', () => { 20 | const parser = new FdbParser(fdbFile, texFile) 21 | const result = parser.parse() 22 | const expectedResult = { 23 | dvips: { 24 | source: ['log-parse/file-pdfps.dvi'], 25 | generated: ['log-parse/file-pdfps.ps'] 26 | }, 27 | latex: { 28 | source: ['file-pdfps.aux', 'file.tex'], 29 | generated: ['log-parse/file-pdfps.aux', 'log-parse/file-pdfps.log', 'log-parse/file-pdfps.dvi'] 30 | }, 31 | ps2pdf: { 32 | source: ['log-parse/file-pdfps.ps'], 33 | generated: ['log-parse/file-pdfps.pdf'] 34 | } 35 | } 36 | 37 | expect(result).toEqual(expectedResult) 38 | }) 39 | }) 40 | 41 | describe('getLines', () => { 42 | it('returns the expected number of lines', () => { 43 | const parser = new FdbParser(fdbFile, texFile) 44 | const lines = parser.getLines() 45 | 46 | expect(lines.length).toBe(17) 47 | }) 48 | 49 | it('throws an error when passed a filepath that does not exist', () => { 50 | const fdbFile = path.join(fixturesPath, 'nope.fdb_latexmk') 51 | const texFile = path.join(fixturesPath, 'nope.tex') 52 | const parser = new FdbParser(fdbFile, texFile) 53 | 54 | expect(parser.getLines).toThrow() 55 | }) 56 | }) 57 | }) 58 | -------------------------------------------------------------------------------- /spec/parsers/log-parser-spec.js: -------------------------------------------------------------------------------- 1 | /** @babel */ 2 | 3 | // eslint-disable-next-line no-unused-vars 4 | import { afterEach, beforeEach, it, fit } from '../async-spec-helpers' 5 | 6 | import _ from 'lodash' 7 | import path from 'path' 8 | import LogParser from '../../lib/parsers/log-parser' 9 | 10 | describe('LogParser', () => { 11 | let fixturesPath 12 | 13 | beforeEach(() => { 14 | fixturesPath = atom.project.getPaths()[0] 15 | }) 16 | 17 | describe('parse', () => { 18 | it('returns the expected output path', () => { 19 | const expectedPath = path.resolve('/foo/output/file.pdf') 20 | const logFile = path.join(fixturesPath, 'file.log') 21 | const texFile = path.join(fixturesPath, 'file.tex') 22 | const parser = new LogParser(logFile, texFile) 23 | const result = parser.parse() 24 | 25 | expect(result.outputFilePath).toBe(expectedPath) 26 | }) 27 | 28 | it('returns the expected output path when the compiled file contained spaces', () => { 29 | const expectedPath = path.resolve('/foo/output/filename with spaces.pdf') 30 | const logFile = path.join(fixturesPath, 'filename with spaces.log') 31 | const texFile = path.join(fixturesPath, 'filename with spaces.tex') 32 | const parser = new LogParser(logFile, texFile) 33 | const result = parser.parse() 34 | 35 | expect(result.outputFilePath).toBe(expectedPath) 36 | }) 37 | 38 | it('parses and returns all errors', () => { 39 | const logFile = path.join(fixturesPath, 'errors.log') 40 | const texFile = path.join(fixturesPath, 'errors.tex') 41 | const parser = new LogParser(logFile, texFile) 42 | const result = parser.parse() 43 | 44 | expect(_.countBy(result.messages, 'type').error).toBe(3) 45 | }) 46 | 47 | it('associates an error with a file path, line number, and message', () => { 48 | const logFile = path.join(fixturesPath, 'errors.log') 49 | const texFile = path.join(fixturesPath, 'errors.tex') 50 | const parser = new LogParser(logFile, texFile) 51 | const result = parser.parse() 52 | const error = result.messages.find(message => { return message.type === 'error' }) 53 | 54 | expect(error).toEqual({ 55 | type: 'error', 56 | logRange: [[196, 0], [196, 84]], 57 | filePath: texFile, 58 | range: [[9, 0], [9, Number.MAX_SAFE_INTEGER]], 59 | logPath: logFile, 60 | text: '\\begin{gather*} on input line 8 ended by \\end{gather}' 61 | }) 62 | }) 63 | }) 64 | 65 | describe('getLines', () => { 66 | it('returns the expected number of lines', () => { 67 | const logFile = path.join(fixturesPath, 'file.log') 68 | const texFile = path.join(fixturesPath, 'file.tex') 69 | const parser = new LogParser(logFile, texFile) 70 | const lines = parser.getLines() 71 | 72 | expect(lines.length).toBe(63) 73 | }) 74 | 75 | it('throws an error when passed a filepath that does not exist', () => { 76 | const logFile = path.join(fixturesPath, 'nope.log') 77 | const texFile = path.join(fixturesPath, 'nope.tex') 78 | const parser = new LogParser(logFile, texFile) 79 | 80 | expect(parser.getLines).toThrow() 81 | }) 82 | }) 83 | }) 84 | -------------------------------------------------------------------------------- /spec/parsers/magic-parser-spec.js: -------------------------------------------------------------------------------- 1 | /** @babel */ 2 | 3 | // eslint-disable-next-line no-unused-vars 4 | import { afterEach, beforeEach, it, fit } from '../async-spec-helpers' 5 | 6 | import path from 'path' 7 | import MagicParser from '../../lib/parsers/magic-parser' 8 | 9 | describe('MagicParser', () => { 10 | let fixturesPath 11 | 12 | beforeEach(() => { 13 | fixturesPath = atom.project.getPaths()[0] 14 | }) 15 | 16 | describe('parse', () => { 17 | it('returns an empty object when file contains no magic comments', () => { 18 | const filePath = path.join(fixturesPath, 'file.tex') 19 | const parser = new MagicParser(filePath) 20 | const result = parser.parse() 21 | 22 | expect(result).toEqual({}) 23 | }) 24 | 25 | it('returns path to root file when file contains magic root comment', () => { 26 | const filePath = path.join(fixturesPath, 'magic-comments', 'root-comment.tex') 27 | const parser = new MagicParser(filePath) 28 | const result = parser.parse() 29 | 30 | expect(result).toEqual({ 31 | 'root': '../file.tex' 32 | }) 33 | }) 34 | 35 | it('returns path to root file when file contains magic root comment when magic comment is not on the first line', () => { 36 | const filePath = path.join(fixturesPath, 'magic-comments', 'not-first-line.tex') 37 | const parser = new MagicParser(filePath) 38 | const result = parser.parse() 39 | 40 | expect(result).toEqual({ 41 | 'root': '../file.tex' 42 | }) 43 | }) 44 | 45 | it('handles magic comments without optional whitespace', () => { 46 | const filePath = path.join(fixturesPath, 'magic-comments', 'no-whitespace.tex') 47 | const parser = new MagicParser(filePath) 48 | const result = parser.parse() 49 | 50 | expect(result).not.toEqual({}) 51 | }) 52 | it('detects multiple object information when multiple magice comments are defined', () => { 53 | const filePath = path.join(fixturesPath, 'magic-comments', 'multiple-magic-comments.tex') 54 | const parser = new MagicParser(filePath) 55 | const result = parser.parse() 56 | 57 | expect(result).toEqual({ 58 | 'root': '../file.tex', 59 | 'program': 'lualatex' 60 | }) 61 | }) 62 | }) 63 | }) 64 | -------------------------------------------------------------------------------- /spec/process-manager-spec.js: -------------------------------------------------------------------------------- 1 | /** @babel */ 2 | 3 | // eslint-disable-next-line no-unused-vars 4 | import { afterEach, beforeEach, it, fit } from './async-spec-helpers' 5 | 6 | import fs from 'fs-plus' 7 | import path from 'path' 8 | import temp from 'temp' 9 | import ProcessManager from '../lib/process-manager' 10 | 11 | describe('ProcessManager', () => { 12 | let processManager 13 | 14 | function constructCommand (fileName) { 15 | const tempPath = fs.realpathSync(temp.mkdirSync('latex')) 16 | const filePath = path.join(tempPath, fileName) 17 | return `latexmk -cd -f -pdf "${filePath}"` 18 | } 19 | 20 | beforeEach(() => { 21 | processManager = new ProcessManager() 22 | }) 23 | 24 | it('kills latexmk when given non-existant file', () => { 25 | let killed = false 26 | 27 | processManager.executeChildProcess(constructCommand('foo.tex'), { allowKill: true }).then(result => { killed = true }) 28 | processManager.killChildProcesses() 29 | 30 | waitsFor(() => killed, 5000) 31 | }) 32 | 33 | it('kills old latexmk instances, but not ones created after the kill command', () => { 34 | let oldKilled = false 35 | let newKilled = false 36 | 37 | processManager.executeChildProcess(constructCommand('old.tex'), { allowKill: true }).then(result => { oldKilled = true }) 38 | processManager.killChildProcesses() 39 | processManager.executeChildProcess(constructCommand('new.tex'), { allowKill: true }).then(result => { newKilled = true }) 40 | 41 | waitsFor(() => oldKilled, 5000) 42 | 43 | runs(() => { 44 | expect(newKilled).toBe(false) 45 | processManager.killChildProcesses() 46 | }) 47 | }) 48 | }) 49 | -------------------------------------------------------------------------------- /spec/spec-helpers.js: -------------------------------------------------------------------------------- 1 | /** @babel */ 2 | 3 | import fs from 'fs-plus' 4 | import temp from 'temp' 5 | import wrench from 'wrench' 6 | 7 | export default { 8 | cloneFixtures () { 9 | const tempPath = fs.realpathSync(temp.mkdirSync('latex')) 10 | let fixturesPath = atom.project.getPaths()[0] 11 | wrench.copyDirSyncRecursive(fixturesPath, tempPath, {forceDelete: true}) 12 | atom.project.setPaths([tempPath]) 13 | fixturesPath = tempPath 14 | 15 | return fixturesPath 16 | }, 17 | 18 | overridePlatform (name) { 19 | Object.defineProperty(process, 'platform', {__proto__: null, value: name}) 20 | }, 21 | 22 | setTimeoutInterval (interval) { 23 | const env = jasmine.getEnv() 24 | const originalInterval = env.defaultTimeoutInterval 25 | env.defaultTimeoutInterval = interval 26 | 27 | return originalInterval 28 | }, 29 | 30 | activatePackages () { 31 | const workspaceElement = atom.views.getView(atom.workspace) 32 | const packages = ['language-latex', 'pdf-view', 'latex'] 33 | const activationPromise = Promise.all(packages.map(pkg => atom.packages.activatePackage(pkg))) 34 | atom.commands.dispatch(workspaceElement, 'latex:sync') 35 | return activationPromise 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /spec/stubs.js: -------------------------------------------------------------------------------- 1 | /** @babel */ 2 | 3 | import Builder from '../lib/builder' 4 | import Logger from '../lib/logger' 5 | import Opener from '../lib/opener' 6 | 7 | export class NullBuilder extends Builder { 8 | static extension = '.tex' 9 | static canProcess (filePath) { return filePath.endsWith(NullBuilder.extension) } 10 | } 11 | 12 | export class NullLogger extends Logger { 13 | error () {} 14 | warning () {} 15 | info () {} 16 | } 17 | 18 | export class NullOpener extends Opener { 19 | open () {} 20 | } 21 | -------------------------------------------------------------------------------- /styles/latex.less: -------------------------------------------------------------------------------- 1 | @import "ui-variables"; 2 | 3 | status-bar { 4 | .latex-status { 5 | .latex-message-count { 6 | padding-left: 0.5em; 7 | } 8 | 9 | .latex-highlight { 10 | color: @text-color-highlight; 11 | } 12 | 13 | .latex-info { 14 | color: @text-color-info; 15 | } 16 | 17 | .latex-warning { 18 | color: @text-color-warning; 19 | } 20 | 21 | .latex-error { 22 | color: @text-color-error; 23 | } 24 | 25 | a { 26 | cursor: pointer; 27 | } 28 | 29 | .icon { 30 | padding-right: 0.125em; 31 | display: inline-block; 32 | } 33 | 34 | &.is-busy { 35 | color: @text-color-highlight; 36 | 37 | a { 38 | color: @text-color-highlight; 39 | } 40 | 41 | .busy.icon::before { 42 | -webkit-animation: spin 2s infinite linear; 43 | text-align: center; 44 | } 45 | } 46 | } 47 | } 48 | 49 | .latex-log { 50 | flex: 1 1 auto; 51 | display: flex; 52 | flex-direction: column; 53 | padding: 0.375em; 54 | 55 | .log-block { 56 | padding: 0.375em; 57 | flex: 0 1 auto; 58 | } 59 | 60 | .log-block.expand { 61 | border: 1px solid @base-border-color; 62 | overflow-y: scroll; 63 | flex: 1 1 auto; 64 | } 65 | 66 | table { 67 | width: 100%; 68 | } 69 | 70 | th { 71 | background-color: @base-background-color; 72 | border: 1px solid @base-border-color; 73 | } 74 | 75 | td { 76 | border: 1px none; 77 | } 78 | 79 | td, th { 80 | vertical-align: top; 81 | padding: .05em .25em .05em .25em; 82 | } 83 | 84 | .latex-info .icon { 85 | color: @text-color-info; 86 | } 87 | 88 | .latex-warning .icon { 89 | color: @text-color-warning; 90 | } 91 | 92 | .latex-error .icon { 93 | color: @text-color-error; 94 | } 95 | 96 | .panel-body { 97 | margin-top: 5px; 98 | overflow-y: scroll; 99 | } 100 | 101 | a.latex-file-reference { 102 | white-space: nowrap; 103 | color: @text-color-subtle; 104 | } 105 | 106 | .latex-highlight { 107 | color: @text-color-highlight; 108 | } 109 | } 110 | 111 | @-webkit-keyframes spin { 112 | from { 113 | -webkit-transform: rotate(0deg); 114 | } 115 | to { 116 | -webkit-transform: rotate(360deg); 117 | } 118 | } 119 | -------------------------------------------------------------------------------- /styles/markers.atom-text-editor.less: -------------------------------------------------------------------------------- 1 | @import "ui-variables"; 2 | 3 | atom-text-editor .gutter .line-number { 4 | /* Use CSS specificity to force errors to override warnings and info markers */ 5 | &.latex-error, &.latex-error.latex-warning, &.latex-error.latex-info, 6 | &.latex-error.latex-warning.latex-info { 7 | border-left: 2px solid @background-color-error; 8 | padding-left: ~"calc(0.5em - 2px)"; 9 | } 10 | /* Use CSS specificity to force warnings to override info markes. */ 11 | &.latex-warning, &.latex-warning.latex-info { 12 | border-left: 2px solid @background-color-warning; 13 | padding-left: ~"calc(0.5em - 2px)"; 14 | } 15 | &.latex-info { 16 | border-left: 2px solid @background-color-info; 17 | padding-left: ~"calc(0.5em - 2px)"; 18 | } 19 | } 20 | --------------------------------------------------------------------------------