Repository: SublimeLinter/SublimeLinter-for-ST2 Branch: master Commit: b62b2ae8ee52 Files: 87 Total size: 1.1 MB Directory structure: gitextract__zefwhfl/ ├── .codeintel/ │ └── config ├── .gitignore ├── Default (Linux).sublime-keymap ├── Default (OSX).sublime-keymap ├── Default (Windows).sublime-keymap ├── Default.sublime-commands ├── LICENSE ├── Main.sublime-menu ├── README.md ├── SublimeLinter.py ├── SublimeLinter.sublime-settings ├── changelog.txt ├── gutter_mark_themes/ │ └── sublime-linter-gutter-markers.psd ├── messages/ │ ├── 1.5.1.txt │ ├── 1.5.2.txt │ ├── 1.5.3.txt │ ├── 1.5.4.txt │ ├── 1.5.5.txt │ ├── 1.5.6.txt │ ├── 1.5.7.txt │ ├── 1.6.0.txt │ ├── 1.6.1.txt │ ├── 1.6.10.txt │ ├── 1.6.11.txt │ ├── 1.6.12.txt │ ├── 1.6.13.txt │ ├── 1.6.2.txt │ ├── 1.6.3.txt │ ├── 1.6.4.txt │ ├── 1.6.5.txt │ ├── 1.6.6.txt │ ├── 1.6.7.txt │ ├── 1.6.8.txt │ ├── 1.6.9.txt │ ├── 1.7.0.txt │ ├── 1.7.1.txt │ ├── 1.7.2.txt │ ├── SublimeLinter3-update1.txt │ ├── SublimeLinter3-update2.txt │ ├── SublimeLinter3-update3.txt │ ├── SublimeLinter3.txt │ └── install.txt ├── messages.json ├── package_control.json └── sublimelinter/ ├── __init__.py ├── loader.py └── modules/ ├── __init__.py ├── base_linter.py ├── c.py ├── c_cpplint.py ├── coffeescript.py ├── css.py ├── git_commit_message.py ├── haml.py ├── haskell.py ├── html.py ├── java.py ├── javascript.py ├── libs/ │ ├── capp_lint.py │ ├── csslint/ │ │ ├── csslint-node.js │ │ └── linter.js │ ├── jsengines/ │ │ ├── jsc.js │ │ └── node.js │ ├── jshint/ │ │ ├── jshint.js │ │ └── linter.js │ ├── jslint/ │ │ ├── jslint.js │ │ └── linter.js │ ├── pep8.py │ └── pyflakes/ │ ├── __init__.py │ ├── __main__.py │ ├── api.py │ ├── checker.py │ ├── messages.py │ └── reporter.py ├── lua.py ├── notes.py ├── objective-j.py ├── perl.py ├── php.py ├── puppet-lint.py ├── puppet.py ├── python.py ├── ruby-lint.py ├── ruby.py ├── squirrel.py ├── sublime_pylint.py └── xml.py ================================================ FILE CONTENTS ================================================ ================================================ FILE: .codeintel/config ================================================ { "Python": { "pythonExtraPaths": [ "libs", "~/Applications/Sublime Text 2.app/Contents/MacOS", "/Applications/Sublime Text 2.app/Contents/MacOS" ] } } ================================================ FILE: .gitignore ================================================ .git .hg .svn *.pyc *.pyo .DS_Store ================================================ FILE: Default (Linux).sublime-keymap ================================================ [ { "keys": ["ctrl+alt+l"], "command": "sublimelinter", "args": {"action": "lint"} }, { "keys": ["ctrl+alt+e"], "command": "find_next_lint_error" }, { "keys": ["ctrl+alt+shift+e"], "command": "find_previous_lint_error" } ] ================================================ FILE: Default (OSX).sublime-keymap ================================================ [ { "keys": ["ctrl+super+l"], "command": "sublimelinter", "args": {"action": "lint"} }, { "keys": ["ctrl+super+e"], "command": "find_next_lint_error" }, { "keys": ["ctrl+super+shift+e"], "command": "find_previous_lint_error" } ] ================================================ FILE: Default (Windows).sublime-keymap ================================================ [ { "keys": ["ctrl+alt+l"], "command": "sublimelinter", "args": {"action": "lint"} }, { "keys": ["ctrl+alt+e"], "command": "find_next_lint_error" }, { "keys": ["ctrl+alt+shift+e"], "command": "find_previous_lint_error" } ] ================================================ FILE: Default.sublime-commands ================================================ [ { "caption": "SublimeLinter: Lint Current File", "command": "sublimelinter_lint", "args": {"action": "lint"} }, { "caption": "SublimeLinter: Show Error List", "command": "sublimelinter_show_errors", "args": {"action": "lint", "show_popup": true} }, { "caption": "SublimeLinter: Background Linting", "command": "sublimelinter_lint", "args": {"action": "on"} }, { "caption": "SublimeLinter: Load-Save Linting", "command": "sublimelinter_enable_load_save", "args": {"action": "load-save"} }, { "caption": "SublimeLinter: Save-Only Linting", "command": "sublimelinter_enable_save_only", "args": {"action": "save-only"} }, { "caption": "SublimeLinter: Disable Linting", "command": "sublimelinter_disable", "args": {"action": "off"} }, { "caption": "SublimeLinter: Extract Annotations", "command": "sublimelinter_annotations", "args": {} }, { "caption": "SublimeLinter: Reset", "command": "sublimelinter_lint", "args": {"action": "reset"} }, { "caption": "Preferences: SublimeLinter Settings – Default", "command": "open_file", "args": { "file": "${packages}/SublimeLinter/SublimeLinter.sublime-settings" } }, { "caption": "Preferences: SublimeLinter Settings – User", "command": "open_file", "args": { "file": "${packages}/User/SublimeLinter.sublime-settings" } } ] ================================================ FILE: LICENSE ================================================ Copyright Germán M. Bravo, Aparajita Fishman, Jacob Swartwood, and other contributors. Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: Main.sublime-menu ================================================ [ { "caption": "Preferences", "mnemonic": "n", "id": "preferences", "children": [ { "caption": "Package Settings", "mnemonic": "P", "id": "package-settings", "children": [ { "caption": "SublimeLinter", "children": [ { "command": "open_file", "args": {"file": "${packages}/SublimeLinter/README.md"}, "caption": "README" }, { "command": "open_file", "args": {"file": "${packages}/SublimeLinter/changelog.txt"}, "caption": "Change Log" }, { "caption": "-" }, { "command": "open_file", "args": {"file": "${packages}/SublimeLinter/SublimeLinter.sublime-settings"}, "caption": "Settings – Default" }, { "command": "open_file", "args": {"file": "${packages}/User/SublimeLinter.sublime-settings"}, "caption": "Settings – User" }, { "command": "open_file_settings", "caption": "Settings – Syntax Specific – User" }, { "caption": "-" }, { "command": "open_file", "args": { "file": "${packages}/SublimeLinter/Default (OSX).sublime-keymap", "platform": "OSX" }, "caption": "Key Bindings – Default" }, { "command": "open_file", "args": { "file": "${packages}/SublimeLinter/Default (Linux).sublime-keymap", "platform": "Linux" }, "caption": "Key Bindings – Default" }, { "command": "open_file", "args": { "file": "${packages}/SublimeLinter/Default (Windows).sublime-keymap", "platform": "Windows" }, "caption": "Key Bindings – Default" }, { "command": "open_file", "args": { "file": "${packages}/User/Default (OSX).sublime-keymap", "platform": "OSX" }, "caption": "Key Bindings – User" }, { "command": "open_file", "args": { "file": "${packages}/User/Default (Linux).sublime-keymap", "platform": "Linux" }, "caption": "Key Bindings – User" }, { "command": "open_file", "args": { "file": "${packages}/User/Default (Windows).sublime-keymap", "platform": "Windows" }, "caption": "Key Bindings – User" }, { "caption": "-" } ] } ] } ] } ] ================================================ FILE: README.md ================================================ SublimeLinter ============= ## SublimeLinter 3 has landed! SublimeLinter for Sublime Text 3 is [here](https://github.com/SublimeLinter/SublimeLinter3), and it’s soooooo much better than before! Install it from Package Control and enjoy! Unless someone else comes forward, SublimeLinter for Sublime Text 2 will no longer be supported. I strongly encourage everyone to upgrade to Sublime Text 3 and SublimeLinter 3 — you’ll be glad you did! Take a look at the [extensive documentation](http://sublimelinter.readthedocs.org/) to see the great new features in SublimeLinter 3. ## Share the love! I spent hundreds of hours writing and documenting SublimeLinter 3 to make it the best it can be — easy to use, easy to configure, easy to update, easy to extend. If you use SublimeLinter and feel it is making your coding life better and easier, please consider making a donation to help fund development and support. Thank you! To donate: https://github.com/SublimeLinter/SublimeLinter3#share-the-love Thank you for your support! --- SublimeLinter v1.7 Overview --------- SublimeLinter is a plugin that supports "lint" programs (known as "linters"). SublimeLinter highlights lines of code the linter deems to contain (potential) errors. It also supports highlighting special annotations (for example: TODO) so that they can be quickly located. SublimeLinter has built in linters for the following languages: * C/C++ - lint via `cppcheck` * CoffeeScript - lint via `coffee -s -l` * CSS - lint via built-in [csslint](http://csslint.net) * Git Commit Messages - lint via built-in module based on [A Note About Git Commit Messages](http://tbaggery.com/2008/04/19/a-note-about-git-commit-messages.html). * Haml - syntax check via `haml -c` * HTML - lint via `tidy` (actually [tidy for HTML5](http://w3c.github.com/tidy-html5/)) * Java - lint via `javac -Xlint` * JavaScript - lint via built in [jshint](http://jshint.org), [jslint](http://jslint.com), or the [closure linter (gjslint)](https://developers.google.com/closure/utilities/docs/linter_howto) (if installed) * Lua - syntax check via `luac` * Objective-J - lint via built-in [capp_lint](https://github.com/aparajita/capp_lint) * Perl - lint via [Perl::Critic](http://perlcritic.com/) or syntax+deprecation check via `perl -c` * PHP - syntax check via `php -l` * Puppet - syntax check via `puppet parser validate` or `puppet-lint` * Python - native, moderately-complete lint * Ruby - syntax check via `ruby -wc` * Squirrel - syntax check via `sq` * XML - lint via `xmllint` Quickstart ------------ * Install using [Package Control ST2 plugin](http://wbond.net/sublime_packages/package_control/installation). * `SublimeLinter` runs in the background (by default), linting files for style and potential errors. * Season to taste (edit configuration) by editing `Preferences->Package Settings->SublimeLinter->Settings - User`. * Produce better code! Installing ---------- **With the Package Control plugin:** The easiest way to install SublimeLinter is through Package Control, which can be found at this site: http://wbond.net/sublime_packages/package_control Once you install Package Control, restart ST2 and bring up the Command Palette (`Command+Shift+P` on OS X, `Control+Shift+P` on Linux/Windows). Select "Package Control: Install Package", wait while Package Control fetches the latest package list, then select SublimeLinter when the list appears. The advantage of using this method is that Package Control will automatically keep SublimeLinter up to date with the latest version. **Without Git:** Download the latest source from [GitHub](https://github.com/SublimeLinter/SublimeLinter) and copy the SublimeLinter folder to your Sublime Text "Packages" directory. **With Git:** Clone the repository in your Sublime Text "Packages" directory: git clone https://github.com/SublimeLinter/SublimeLinter.git The "Packages" directory is located at: * OS X: ~/Library/Application Support/Sublime Text 2/Packages/ * Linux: ~/.config/sublime-text-2/Packages/ * Windows: %APPDATA%/Sublime Text 2/Packages/ ### JavaScript-based linters If you plan to edit files that use a JavaScript-based linter (JavaScript, CSS), your system must have a JavaScript engine installed. Mac OS X comes with a preinstalled JavaScript engine called JavaScriptCore, which is used if Node.js is not installed. On Windows, you **must** install the JavaScript engine Node.js, which can be downloaded from [the Node.js site](http://nodejs.org/#download). On Mac OS X, you **must** install Node.js if you plan to edit JavaScript or CSS files that use non-ASCII characters in strings or comments, because JavaScriptCore is not Unicode-aware. After installing Node.js, if the Node.js executable ("node" on Mac OS X, "node.exe" on Windows) cannot be found by SublimeLinter, you may have to set the path to the executable in the "sublimelinter\_executable\_map" setting. See the "Configuring" section below for info on SublimeLinter settings. Using ----- SublimeLinter runs in one of three modes, which is determined by the "sublimelinter" user setting: * **Background mode (the default)** - When the "sublimelinter" setting is true, linting is performed in the background as you modify a file (if the relevant linter supports it). If you like instant feedback, this is the best way to use SublimeLinter. If you want feedback, but not instantly, you can try another mode or set a minimum queue delay with the "sublimelinter_delay" setting, so that the linter will only run after a certain amount of idle time. * **Load-save mode** - When the "sublimelinter" setting is "load-save", linting is performed only when a file is loaded and after saving. Errors are cleared as soon as the file is modified. * **Save-only mode** - When the "sublimelinter" setting is "save-only", linting is performed only after a file is saved. Errors are cleared as soon as the file is modified. * **On demand mode** - When the "sublimelinter" setting is false, linting is performed only when initiated by you. Use the `Control+Command+L` (OS X) or `Control+Alt+L` (Linux/Windows) key equivalent or the Command Palette to lint the current file. If the current file has no associated linter, the command will not be available. Within a file whose language/syntax is supported by SublimeLinter, you can control SublimeLinter via the Command Palette (`Command+Shift+P` on OS X, `Control+Shift+P` on Linux/Windows). The available commands are: * **SublimeLinter: Lint Current File** - Lints the current file, highlights any errors and displays how many errors were found. * **SublimeLinter: Show Error List** - Lints the current file, highlights any errors and displays a quick panel with any errors that are found. Selecting an item from the quick panel jumps to that line. * **SublimeLinter: Background Linting** - Enables background linting mode for the current view and lints it. * **SublimeLinter: Disable Linting** - Disables linting mode for the current view and clears all lint errors. * **SublimeLinter: Load-Save Linting** - Enables load-save linting mode for the current view and clears all lint errors. * **SublimeLinter: Save-Only Linting** - Enables save-only linting mode for the current view and clears all lint errors. * **SublimeLinter: Reset** - Clears all lint errors and sets the linting mode to the value in the SublimeLinter.sublime-settings file. Depending on the file and the current state of background enabling, some of the commands will not be available. When an error is highlighted by the linter, putting the cursor on the offending line will result in the error message being displayed on the status bar. If you want to be shown a popup list of all errors whenever a file is saved, modify the user setting: "sublimelinter_popup_errors_on_save": true If there are errors in the file, a quick panel will appear which shows the error message, line number and source code for each error. The starting location of all errors on the line are marked with "^". Selecting an error in the quick panel jumps directly to the location of the first error on that line. While editing a file, you can quickly move to the next/previous lint error with the following key equivalents: * **OS X**: next: Control+Command+E prev: Control+Command+Shift+E * **Linux, Windows**: next: Control+Alt+E prev: Control+Alt+Shift+E By default the search will wrap. You can turn wrapping off with the user setting: "sublimelinter_wrap_find": false Please note: these key commands may conflict with other important cmds (such as generating the € character - this was discussed in issue [#182](https://github.com/SublimeLinter/SublimeLinter/issues/182)). If these controls are problematic, you may always adjust your settings by copying the defaults stored in `Preferences->Package Settings->SublimeLinter->Key Bindings - Default` into `Preferences->Key Bindings - User` and then modifying the values appropriately. Configuring ----------- There are a number of settings available to customize the behavior of SublimeLinter and its linters. For the latest information on what settings are available, select the menu item `Preferences->Package Settings->SublimeLinter->Settings - Default`. Do **NOT** edit the default SublimeLinter settings. Your changes will be lost when SublimeLinter is updated. ALWAYS edit the user SublimeLinter settings by selecting `Preferences->Package Settings->SublimeLinter->Settings - User`. Note that individual settings you include in your user settings will _completely_ replace the corresponding default setting, so you must provide that setting in its entirety. ### Linter-specific notes Following are notes specific to individual linters that you should be aware of: * **C/C++** - The default C/C++ linter is [cppcheck](http://cppcheck.sourceforge.net/), however Google's [cpplint.py](http://google-styleguide.googlecode.com/svn/trunk/cpplint/) is also supported. To swap `cppcheck` out for `cpplint.py` you will need to adjust `sublimelinter_syntax_map` and possibly `sublimelinter_executable_map` also. First change the _linter language_ for `C` and `C++` to `c_cpplint` via `sublimelinter_syntax_map`. If `cpplint.py` is not on your system `PATH`, then add an entry for `c_cpplint` into `sublimelinter_executable_map` with the path to the file. As usual add these adjustments to the SublimeLinter **User Settings** file. An example: "sublimelinter_syntax_map": { "Python Django": "python", "Ruby on Rails": "ruby", "C++": "c_cpplint", "C": "c_cpplint" }, "sublimelinter_executable_map": { "c_cpplint": "/Users/[my username]/Desktop/cpplint.py" } * **CSS** - This linter runs [csslint](http://csslint.net). This linter requires a JavaScript engine (like Node.js) to be installed (see notes above for the JavaScript linters: "jshint" or "jslint"). By default all CSSLint settings are turned on. You may customize CSSLint behavior with the "csslint_options" setting. Please select `Preferences->Package Settings->SublimeLinter->Settings - Default` for more information on turning off or adjusting severity of tests. For more information about options available to CSSLint, see https://github.com/stubbornella/csslint/wiki/Rules. * **HTML** - This linter will not run unless you have a version of tidy with HTML5 support. To use this linter, please see: https://github.com/w3c/tidy-html5 * **Java** - Because it uses `javac` to do linting, each time you run the linter the entire dependency graph of the current file will be checked. Depending on the number of classes you import, this can be **extremely** slow. Also note that you **must** provide the `-sourcepath`, `-classpath`, `-Xlint` and `{filename}` arguments to `javac` in your per-project settings. See "Per-project settings" below for more information. * **JavaScript** - If the "javascript_linter" setting is "jshint" or "jslint", this linter runs [jshint](http://jshint.org) (or [jslint](http://jslint.com) respectively) using Node.js. See "JavaScript-based linters" above for information on how to install Node.js. If the "javascript_linter" setting is "gjslint", this linter runs the [closure linter (gjslint)](https://developers.google.com/closure/utilities/docs/linter_howto). After installation, if gjslint cannot be found by SublimeLinter, you may have to set the path to gjslint in the "sublimelinter\_executable\_map" setting. You may want to modify the options passed to jshint, jslint, or gjslint. This can be done by using the **jshint_options**, **jslint_options**, or **gjslint_options** setting. Refer to the jshint.org site, the jslint.com site, or run `gjslint --help` for more information on the configuration options available. SublimeLinter supports `.jshintrc` files. If using JSHint, SublimeLinter will recursively search the directory tree (from the file location to the file-system root directory). This functionality is specified in the [JSHint README](https://github.com/jshint/node-jshint/#within-your-projects-directory-tree). * **Perl** - Due to a vulnerability (issue [#77](https://github.com/SublimeLinter/SublimeLinter/issues/77)) with the Perl linter, Perl syntax checking is no longer enabled by default. The default linter for Perl has been replaced by Perl::Critic. The standard Perl syntax checker can still be invoked by switching the "perl_linter" setting to "perl". * **Puppet** - Optional alternative linter using puppet-lint. Install with `gem install puppet-lint`. Add these adjustments to the SublimeLinter **User Settings** file. An example: "sublimelinter_syntax_map": { "Puppet": "puppet-lint" }. Note this only lints on file save. * **Ruby** - If you are using rvm or rbenv, you will probably have to specify the full path to the ruby you are using in the "sublimelinter_executable_map" setting. See "Configuring" below for more info. ### Per-project settings SublimeLinter supports per-project/per-language settings. This is useful if a linter requires path configuration on a per-project basis. To edit your project settings, select the menu item `Project->Edit Project`. If there is no "settings" object at the top level, add one and then add a "SublimeLinter" sub-object, like this: { "folders": [ { "path": "/Users/aparajita/Projects/foo/src" } ], "settings": { "SublimeLinter": { } } } Within the "SublimeLinter" object, you can add a settings object for each language. The language name must match the language item in the linter's CONFIG object, which can be found in the linter's source file in the SublimeLinter/sublimelinter/modules folder. Each language can have two settings: * "working_directory" - If present and a valid absolute directory path, the working directory is set to this path before the linter executes. This is useful if you are providing linter arguments that contain paths and you want to use working directory-relative paths instead of absolute paths. * "lint_args" - If present, it must be a sequence of string arguments to pass to the linter. If your linter expects a filename as an argument, use the argument "{filename}" as a placeholder. If it expects stdin, use "-". Note that if you provide this item, you are responsible for passing **all** required arguments to the linter, as it will override default arguments. For example, let's say we are editing a Java project and want to use the "java" linter, which requires a source path and class path. In addition, we want to ignore serialization errors. Our project settings might look like this: { "folders": [ { "path": "/Users/aparajita/Projects/foo/src" } ], "settings": { "SublimeLinter": { "Java": { "working_directory": "/Users/aparajita/Projects/foo", "lint_args": [ "-sourcepath", "src", "-classpath", "libs/log4j-1.2.9.jar:libs/commons-logging-1.1.jar", "-Xlint", "-Xlint:-serial", "{filename}" ] } } } } The jshint follows convention set by node-jshint (though node is not required) and will attempt to locate the configuration file for you starting in pwd. (or "present working directory") If this does not yield a .jshintrc file, it will move one level up (..) the directory tree all the way up to the filesystem root. If a file is found, it stops immediately and uses that set of configuration instead of "jshint_options". ### Customizing colors **IMPORTANT** - The theme style names have recently changed. The old and new color names are: Old New --------------------- ----------------------------- sublimelinter. sublimelinter.outline. invalid. sublimelinter.underline. Please change the names in your color themes accordingly. There are three types of "errors" flagged by SublimeLinter: illegal, violation, and warning. For each type, SublimeLinter will indicate the offending line and the character position at which the error occurred on the line. By default SublimeLinter will outline offending lines using the background color of the "sublimelinter.outline." theme style, and underline the character position using the background color of the "sublimelinter.underline." theme style, where is one of the three error types. If these styles are not defined, the color will be black when there is a light background color and black when there is a dark background color. You may define a single "sublimelinter.outline" or "sublimelinter.underline" style to color all three types, or define separate substyles for one or more types to color them differently. If you want to make the offending lines glaringly obvious (perhaps for those who tend to ignore lint errors), you can set the user setting: "sublimelinter_mark_style": "fill" When this is set true, lines that have errors will be colored with the background and foreground color of the "sublime.outline." theme style. Unless you have defined those styles, this setting should be left as "outline". You may want to disable drawing of outline boxes entirely. If so, change using the user setting to: "sublimelinter_mark_style": "none" You may also mark lines with errors by putting an "x" in the gutter with the user setting: "sublimelinter_gutter_marks": true To customize the colors used for highlighting errors and user notes, add the following to your theme (adapting the color to your liking): name SublimeLinter Annotations scope sublimelinter.annotations settings background #FFFFAA foreground #FFFFFF name SublimeLinter Error Outline scope sublimelinter.outline.illegal settings background #FF4A52 foreground #FFFFFF name SublimeLinter Error Underline scope sublimelinter.underline.illegal settings background #FF0000 name SublimeLinter Warning Outline scope sublimelinter.outline.warning settings background #DF9400 foreground #FFFFFF name SublimeLinter Warning Underline scope sublimelinter.underline.warning settings background #FF0000 name SublimeLinter Violation Outline scope sublimelinter.outline.violation settings background #ffffff33 foreground #FFFFFF name SublimeLinter Violation Underline scope sublimelinter.underline.violation settings background #FF0000 Troubleshooting --------------- If a linter does not seem to be working, you can check the ST2 console to see if it was enabled. When SublimeLinter is loaded, you will see messages in the console like this: Reloading plugin /Users/aparajita/Library/Application Support/Sublime Text 2/Packages/SublimeLinter/sublimelinter_plugin.py SublimeLinter: JavaScript loaded SublimeLinter: annotations loaded SublimeLinter: Objective-J loaded SublimeLinter: perl loaded SublimeLinter: php loaded SublimeLinter: python loaded SublimeLinter: ruby loaded SublimeLinter: pylint loaded The first time a linter is asked to lint, it will check to see if it can be enabled. You will then see messages like this: SublimeLinter: JavaScript enabled (using JavaScriptCore) SublimeLinter: Ruby enabled (using "ruby" for executable) Let's say the ruby linter is not working. If you look at the console, you may see a message like this: SublimeLinter: ruby disabled ("ruby" cannot be found) This means that the ruby executable cannot be found on your system, which means it is not installed or not in your executable path. Creating New Linters -------------------- If you wish to create a new linter to support a new language, SublimeLinter makes it easy. Here are the steps involved: * Create a new file in sublimelinter/modules. If your linter uses an external executable, you will probably want to copy perl.py. If your linter uses built in code, copy objective-j.py. The convention is to name the file the same as the language that will be linted. * Configure the CONFIG dict in your module. See the comments in base\_linter.py for information on the values in that dict. You only need to set the values in your module that differ from the defaults in base\_linter.py, as your module's CONFIG is merged with the default. Note that if your linter uses an external executable that does not take stdin, setting 'input\_method' to INPUT\_METHOD\_TEMP\_FILE will allow interactive linting with that executable. * If your linter uses built in code, override `built_in_check()` and return the errors found. * Override `parse_errors()` and process the errors. If your linter overrides `built_in_check()`, `parse_errors()` will receive the result of that method. If your linter uses an external executable, `parse_errors()` receives the raw output of the executable, stripped of leading and trailing whitespace. * If you linter is powered via JavaScript (eg. Node.js), there are few steps that will simplify the integration. Create a folder matching your linter name in the `SublimeLinter/sublimelinter/modules/lib` directory. This folder should include the linting library JS file (eg. jshint.js, csslint-Node.js) and a **linter.js** file. The **linter.js** file should `require()` the actual linter library file and export a `lint()` function. The `lint()` function should return a list of errors back to the python language handler file (via the `errors` parameter to the `parse_errors()` method). Although **linter.js** should follow the Node.js api, the linter may also be run via JavaScriptCore on OS X if Node.js is not installed. In the case where JavaScriptCore is used, require + export are shimmed to keep things consistent. However, it is important not to assume that a full Node.js api is available. If you must know what JS engine you are using, you may check for `USING_JSC` to be set as `true` when JavaScriptCore is used. For examples of using the JS engines, see **csslint**, **jslint**, and **jshint** in `SublimeLinter/sublimelinter/modules/libs` and the respective python code of **css.py** and **javascript.py** in `SublimeLinter/sublimelinter/modules`. If your linter has more complex requirements, see the comments for CONFIG in base\_linter.py, and use the existing linters as guides. Beta ---- The SublimeLinter Beta package is available via Package Control. The beta version is considered unstable and should only be used for testing the next release. If you like living on the edge, please report any bugs you find on the [SublimeLinter issues](https://github.com/SublimeLinter/SublimeLinter/issues) page. ================================================ FILE: SublimeLinter.py ================================================ from functools import partial import os import re import sys import time import threading import sublime import sublime_plugin from sublimelinter.loader import Loader from sublimelinter.modules.base_linter import INPUT_METHOD_FILE LINTERS = {} # mapping of language name to linter module QUEUE = {} # views waiting to be processed by linter ERRORS = {} # error messages on given line obtained from linter; they are # displayed in the status bar when cursor is on line with error VIOLATIONS = {} # violation messages, they are displayed in the status bar WARNINGS = {} # warning messages, they are displayed in the status bar UNDERLINES = {} # underline regions related to each lint message TIMES = {} # collects how long it took the linting to complete MOD_LOAD = Loader(os.getcwdu(), LINTERS) # utility to load (and reload # if necessary) linter modules [useful when working on plugin] # For snappier linting, different delays are used for different linting times: # (linting time, delays) DELAYS = ( (50, (50, 100)), (100, (100, 300)), (200, (200, 500)), (400, (400, 1000)), (800, (800, 2000)), (1600, (1600, 3000)), ) # Select one of the predefined gutter mark themes, the options are: # "alpha", "bright", "dark", "hard" and "simple" MARK_THEMES = ('alpha', 'bright', 'dark', 'hard', 'simple') # The path to the built-in gutter mark themes MARK_THEMES_PATH = os.path.join('..', 'SublimeLinter', 'gutter_mark_themes') # The original theme for anyone interested the previous minimalist approach ORIGINAL_MARK_THEME = { 'violation': 'dot', 'warning': 'dot', 'illegal': 'circle' } # All available settings for SublimeLinter; # only these are inherited from SublimeLinter.sublime-settings ALL_SETTINGS = [ 'annotations', 'csslint_options', 'gjslint_ignore', 'gjslint_options', 'javascript_linter', 'jshint_options', 'jslint_options', 'pep8', 'pep8_ignore', 'perl_linter', 'pyflakes_ignore', 'pyflakes_ignore_import_*', 'sublimelinter', 'sublimelinter_delay', 'sublimelinter_disable', 'sublimelinter_executable_map', 'sublimelinter_fill_outlines', 'sublimelinter_gutter_marks', 'sublimelinter_gutter_marks_theme', 'sublimelinter_mark_style', 'sublimelinter_notes', 'sublimelinter_objj_check_ascii', 'sublimelinter_popup_errors_on_save', 'sublimelinter_syntax_map', 'sublimelinter_wrap_find', ] WHITESPACE_RE = re.compile(r'\s+') def get_delay(t, view): delay = 0 for _t, d in DELAYS: if _t <= t: delay = d else: break delay = delay or DELAYS[0][1] # If the user specifies a delay greater than the built in delay, # figure they only want to see marks when idle. minDelay = int(view.settings().get('sublimelinter_delay', 0) * 1000) if minDelay > delay[1]: erase_lint_marks(view) return (minDelay, minDelay) if minDelay > delay[1] else delay def last_selected_lineno(view): viewSel = view.sel() if not viewSel: return None return view.rowcol(viewSel[0].end())[0] def update_statusbar(view): vid = view.id() lineno = last_selected_lineno(view) errors = [] if lineno is not None: if vid in ERRORS and lineno in ERRORS[vid]: errors.extend(ERRORS[vid][lineno]) if vid in VIOLATIONS and lineno in VIOLATIONS[vid]: errors.extend(VIOLATIONS[vid][lineno]) if vid in WARNINGS and lineno in WARNINGS[vid]: errors.extend(WARNINGS[vid][lineno]) if errors: view.set_status('Linter', '; '.join(errors)) else: view.erase_status('Linter') def run_once(linter, view, **kwargs): '''run a linter on a given view regardless of user setting''' if not linter: return vid = view.id() ERRORS[vid] = {} VIOLATIONS[vid] = {} WARNINGS[vid] = {} start = time.time() text = view.substr(sublime.Region(0, view.size())).encode('utf-8') lines, error_underlines, violation_underlines, warning_underlines, ERRORS[vid], VIOLATIONS[vid], WARNINGS[vid] = linter.run(view, text, (view.file_name() or '').encode('utf-8')) UNDERLINES[vid] = error_underlines[:] UNDERLINES[vid].extend(violation_underlines) UNDERLINES[vid].extend(warning_underlines) add_lint_marks(view, lines, error_underlines, violation_underlines, warning_underlines) if view.settings().get('sublimelinter_notes'): highlight_notes(view) update_statusbar(view) end = time.time() TIMES[vid] = (end - start) * 1000 # Keep how long it took to lint if kwargs.get('event', None) == 'on_post_save' and view.settings().get('sublimelinter_popup_errors_on_save'): popup_error_list(view) def popup_error_list(view): vid = view.id() errors = ERRORS[vid].copy() for message_map in [VIOLATIONS[vid], WARNINGS[vid]]: for line, messages in message_map.items(): if line in errors: errors[line].extend(messages) else: errors[line] = messages # Flatten the errors into a list error_list = [] for line in sorted(errors.keys()): for index, message in enumerate(errors[line]): error_list.append({'line': line, 'message': message}) panel_items = [] for error in error_list: line_text = view.substr(view.full_line(view.text_point(error['line'], 0))) item = [error['message'], u'{0}: {1}'.format(error['line'] + 1, line_text.strip())] panel_items.append(item) def on_done(selected_item): if selected_item == -1: return selected = view.sel() selected.clear() error = error_list[selected_item] region_begin = view.text_point(error['line'], 0) # Go to the first non-whitespace character of the line line_text = view.substr(view.full_line(region_begin)) match = WHITESPACE_RE.match(line_text) if (match): region_begin += len(match.group(0)) selected.add(sublime.Region(region_begin, region_begin)) # We have to force a move to update the cursor position view.run_command('move', {'by': 'characters', 'forward': True}) view.run_command('move', {'by': 'characters', 'forward': False}) view.show_at_center(region_begin) view.window().show_quick_panel(panel_items, on_done) def add_lint_marks(view, lines, error_underlines, violation_underlines, warning_underlines): '''Adds lint marks to view.''' vid = view.id() erase_lint_marks(view) types = {'warning': warning_underlines, 'violation': violation_underlines, 'illegal': error_underlines} for type_name, underlines in types.items(): if underlines: view.add_regions('lint-underline-' + type_name, underlines, 'sublimelinter.underline.' + type_name, sublime.DRAW_EMPTY_AS_OVERWRITE) if lines: outline_style = view.settings().get('sublimelinter_mark_style', 'outline') # This test is for the legacy "fill" setting; it will be removed # in a future version (likely v1.7). if view.settings().get('sublimelinter_fill_outlines', False): outline_style = 'fill' gutter_mark_enabled = True if view.settings().get('sublimelinter_gutter_marks', False) else False gutter_mark_theme = view.settings().get('sublimelinter_gutter_marks_theme', 'simple') outlines = {'warning': [], 'violation': [], 'illegal': []} for line in ERRORS[vid]: outlines['illegal'].append(view.full_line(view.text_point(line, 0))) for line in WARNINGS[vid]: outlines['warning'].append(view.full_line(view.text_point(line, 0))) for line in VIOLATIONS[vid]: outlines['violation'].append(view.full_line(view.text_point(line, 0))) for lint_type in outlines: if outlines[lint_type]: args = [ 'lint-outlines-{0}'.format(lint_type), outlines[lint_type], 'sublimelinter.outline.{0}'.format(lint_type) ] gutter_mark_image = '' if gutter_mark_enabled: if gutter_mark_theme == 'original': gutter_mark_image = ORIGINAL_MARK_THEME[lint_type] elif gutter_mark_theme in MARK_THEMES: gutter_mark_image = os.path.join(MARK_THEMES_PATH, gutter_mark_theme + '-' + lint_type) else: gutter_mark_image = gutter_mark_theme + '-' + lint_type args.append(gutter_mark_image) if outline_style == 'none': args.append(sublime.HIDDEN) elif outline_style == 'fill': pass # outlines are filled by default else: args.append(sublime.DRAW_OUTLINED) view.add_regions(*args) def erase_lint_marks(view): '''erase all "lint" error marks from view''' view.erase_regions('lint-underline-illegal') view.erase_regions('lint-underline-violation') view.erase_regions('lint-underline-warning') view.erase_regions('lint-outlines-illegal') view.erase_regions('lint-outlines-violation') view.erase_regions('lint-outlines-warning') view.erase_regions('lint-annotations') def get_lint_regions(view, reverse=False, coalesce=False): vid = view.id() underlines = UNDERLINES.get(vid, [])[:] if (coalesce): # Each of these regions is one character, so transform it into the character points points = sorted([region.begin() for region in underlines]) # Now coalesce adjacent characters into a single region underlines = [] last_point = -999 for point in points: if point != last_point + 1: underlines.append(sublime.Region(point, point)) else: region = underlines[-1] underlines[-1] = sublime.Region(region.begin(), point) last_point = point # Now get all outlines, which includes the entire line where underlines are outlines = view.get_regions('lint-outlines-illegal') outlines.extend(view.get_regions('lint-outlines-violation')) outlines.extend(view.get_regions('lint-outlines-warning')) outlines.extend(view.get_regions('lint-annotations')) # If an outline region contains an underline region, use only the underline regions = underlines for outline in outlines: contains_underlines = False for underline in underlines: if outline.contains(underline): contains_underlines = True break if not contains_underlines: regions.append(outline) return sorted(regions, key=lambda x: x.begin(), reverse=reverse) def select_lint_region(view, region): selected = view.sel() selected.clear() # Find the first underline region within the region to select. # If there are none, put the cursor at the beginning of the line. underlineRegion = find_underline_within(view, region) if underlineRegion is None: underlineRegion = sublime.Region(region.begin(), region.begin()) selected.add(underlineRegion) view.show(underlineRegion, True) def find_underline_within(view, region): underlines = view.get_regions('lint-underline-illegal') underlines.extend(view.get_regions('lint-underline-violation')) underlines.extend(view.get_regions('lint-underline-warning')) underlines.sort(key=lambda x: x.begin()) for underline in underlines: if region.contains(underline): return underline return None def syntax_name(view): syntax = os.path.basename(view.settings().get('syntax')) syntax = os.path.splitext(syntax)[0] return syntax def select_linter(view, ignore_disabled=False): '''selects the appropriate linter to use based on language in current view''' syntax = syntax_name(view) lc_syntax = syntax.lower() language = None linter = None syntaxMap = view.settings().get('sublimelinter_syntax_map', {}) if syntax in syntaxMap: language = syntaxMap.get(syntax, '').lower() elif lc_syntax in syntaxMap: language = syntaxMap.get(lc_syntax, '').lower() elif lc_syntax in LINTERS: language = lc_syntax if language: if ignore_disabled: disabled = [] else: disabled = view.settings().get('sublimelinter_disable', []) if language not in disabled: linter = LINTERS.get(language) # If the enabled state is False, it must be checked. # Enabled checking has to be deferred to first view use because # user settings cannot be loaded during plugin startup. if linter is not None and not linter.enabled: enabled, message = linter.check_enabled(view) print 'SublimeLinter: {0} {1} ({2})'.format(language, 'enabled' if enabled else 'disabled', message) if not enabled: del LINTERS['' + language] linter = None return linter def highlight_notes(view): '''highlight user-specified annotations in a file''' view.erase_regions('lint-annotations') text = view.substr(sublime.Region(0, view.size())) regions = LINTERS['annotations'].built_in_check(view, text, '') if regions: view.add_regions('lint-annotations', regions, 'sublimelinter.annotations', sublime.DRAW_EMPTY_AS_OVERWRITE) def _update_view(view, filename, **kwargs): # It is possible that by the time the queue is run, # the original file is no longer being displayed in the view, # or the view may be gone. This happens especially when # viewing files temporarily by single-clicking on a filename # in the sidebar or when selecting a file through the choose file palette. valid_view = False view_id = view.id() for window in sublime.windows(): for v in window.views(): if v.id() == view_id: valid_view = True break if not valid_view or view.is_loading() or (view.file_name() or '').encode('utf-8') != filename: return try: run_once(select_linter(view), view, **kwargs) except RuntimeError, ex: print ex def queue_linter(linter, view, timeout=-1, preemptive=False, event=None): '''Put the current view in a queue to be examined by a linter''' if linter is None: erase_lint_marks(view) # may have changed file type and left marks behind # No point in queuing anything if no linters will run if not view.settings().get('sublimelinter_notes'): return if preemptive: timeout = busy_timeout = 0 elif timeout == -1: timeout, busy_timeout = get_delay(TIMES.get(view.id(), 100), view) else: busy_timeout = timeout kwargs = {'timeout': timeout, 'busy_timeout': busy_timeout, 'preemptive': preemptive, 'event': event} queue(view, partial(_update_view, view, (view.file_name() or '').encode('utf-8'), **kwargs), kwargs) def _callback(view, filename, kwargs): kwargs['callback'](view, filename, **kwargs) def background_linter(): __lock_.acquire() try: callbacks = QUEUE.values() QUEUE.clear() finally: __lock_.release() for callback in callbacks: sublime.set_timeout(callback, 0) ################################################################################ # Queue dispatcher system: queue_dispatcher = background_linter queue_thread_name = 'background linter' MAX_DELAY = 10 def queue_loop(): '''An infinite loop running the linter in a background thread meant to update the view after user modifies it and then does no further modifications for some time as to not slow down the UI with linting.''' global __signaled_, __signaled_first_ while __loop_: #print 'acquire...' __semaphore_.acquire() __signaled_first_ = 0 __signaled_ = 0 #print 'DISPATCHING!', len(QUEUE) queue_dispatcher() def queue(view, callback, kwargs): global __signaled_, __signaled_first_ now = time.time() __lock_.acquire() try: QUEUE[view.id()] = callback timeout = kwargs['timeout'] busy_timeout = kwargs['busy_timeout'] if now < __signaled_ + timeout * 4: timeout = busy_timeout or timeout __signaled_ = now _delay_queue(timeout, kwargs['preemptive']) if not __signaled_first_: __signaled_first_ = __signaled_ #print 'first', #print 'queued in', (__signaled_ - now) finally: __lock_.release() def _delay_queue(timeout, preemptive): global __signaled_, __queued_ now = time.time() if not preemptive and now <= __queued_ + 0.01: return # never delay queues too fast (except preemptively) __queued_ = now _timeout = float(timeout) / 1000 if __signaled_first_: if MAX_DELAY > 0 and now - __signaled_first_ + _timeout > MAX_DELAY: _timeout -= now - __signaled_first_ if _timeout < 0: _timeout = 0 timeout = int(round(_timeout * 1000, 0)) new__signaled_ = now + _timeout - 0.01 if __signaled_ >= now - 0.01 and (preemptive or new__signaled_ >= __signaled_ - 0.01): __signaled_ = new__signaled_ #print 'delayed to', (preemptive, __signaled_ - now) def _signal(): if time.time() < __signaled_: return __semaphore_.release() sublime.set_timeout(_signal, timeout) def delay_queue(timeout): __lock_.acquire() try: _delay_queue(timeout, False) finally: __lock_.release() # only start the thread once - otherwise the plugin will get laggy # when saving it often. __semaphore_ = threading.Semaphore(0) __lock_ = threading.Lock() __queued_ = 0 __signaled_ = 0 __signaled_first_ = 0 # First finalize old standing threads: __loop_ = False __pre_initialized_ = False def queue_finalize(timeout=None): global __pre_initialized_ for thread in threading.enumerate(): if thread.isAlive() and thread.name == queue_thread_name: __pre_initialized_ = True thread.__semaphore_.release() thread.join(timeout) queue_finalize() # Initialize background thread: __loop_ = True __active_linter_thread = threading.Thread(target=queue_loop, name=queue_thread_name) __active_linter_thread.__semaphore_ = __semaphore_ __active_linter_thread.start() ################################################################################ UNRECOGNIZED = ''' * Unrecognized option * : %s ============================================== ''' def view_in_tab(view, title, text, file_type): '''Helper function to display information in a tab. ''' tab = view.window().new_file() tab.set_name(title) _id = tab.buffer_id() tab.set_scratch(_id) tab.settings().set('gutter', True) tab.settings().set('line_numbers', False) tab.set_syntax_file(file_type) ed = tab.begin_edit() tab.insert(ed, 0, text) tab.end_edit(ed) return tab, _id def lint_views(linter): if not linter: return viewsToLint = [] for window in sublime.windows(): for view in window.views(): viewLinter = select_linter(view) if viewLinter == linter: viewsToLint.append(view) for view in viewsToLint: queue_linter(linter, view, preemptive=True) def reload_view_module(view): for name, linter in LINTERS.items(): module = sys.modules[linter.__module__] if module.__file__.encode('utf-8') == (view.file_name() or '').encode('utf-8'): print 'SublimeLinter: reloading language:', linter.language MOD_LOAD.reload_module(module) lint_views(linter) break def settings_changed(): for window in sublime.windows(): for view in window.views(): linter = select_linter(view) if (linter): reload_settings(view) def reload_settings(view): '''Restores user settings.''' settings = sublime.load_settings(__name__ + '.sublime-settings') settings.clear_on_change(__name__) settings.add_on_change(__name__, settings_changed) for setting in ALL_SETTINGS: if settings.get(setting) != None: view.settings().set(setting, settings.get(setting)) if view.settings().get('sublimelinter') == None: view.settings().set('sublimelinter', True) class LintCommand(sublime_plugin.TextCommand): '''command to interact with linters''' def __init__(self, view): self.view = view self.help_called = False def run_(self, action): '''method called by default via view.run_command; used to dispatch to appropriate method''' if not action: return try: lc_action = action.lower() except AttributeError: return if lc_action == 'reset': self.reset() elif lc_action == 'on': self.on() elif lc_action == 'load-save': self.enable_load_save() elif lc_action == 'save-only': self.enable_save_only() elif lc_action == 'off': self.off() elif action.lower() in LINTERS: self._run(lc_action) def reset(self): '''Removes existing lint marks and restores user settings.''' erase_lint_marks(self.view) reload_settings(self.view) def on(self): '''Turns background linting on.''' self.view.settings().set('sublimelinter', True) queue_linter(select_linter(self.view), self.view, preemptive=True) def enable_load_save(self): '''Turns load-save linting on.''' self.view.settings().set('sublimelinter', 'load-save') erase_lint_marks(self.view) def enable_save_only(self): '''Turns save-only linting on.''' self.view.settings().set('sublimelinter', 'save-only') erase_lint_marks(self.view) def off(self): '''Turns background linting off.''' self.view.settings().set('sublimelinter', False) erase_lint_marks(self.view) def _run(self, name): '''runs an existing linter''' run_once(LINTERS[name.lower()], self.view) class BackgroundLinter(sublime_plugin.EventListener): '''This plugin controls a linter meant to work in the background to provide interactive feedback as a file is edited. It can be turned off via a setting. ''' def __init__(self): super(BackgroundLinter, self).__init__() self.lastSelectedLineNo = -1 def on_modified(self, view): if view.is_scratch(): return if view.settings().get('sublimelinter') != True: erase_lint_marks(view) return linter = select_linter(view) # File-based linters are not invoked during a modify if linter and linter.input_method == INPUT_METHOD_FILE: erase_lint_marks(view) return # Reset the last selected line number so that the current line will show error messages # when update_statusbar is called. self.lastSelectedLineNo = -1 queue_linter(linter, view) def on_load(self, view): reload_settings(view) sublimelinter_setting = view.settings().get('sublimelinter') if view.is_scratch() or sublimelinter_setting == False or sublimelinter_setting == 'save-only': return queue_linter(select_linter(view), view, event='on_load') def on_post_save(self, view): sublimelinter_setting = view.settings().get('sublimelinter') if sublimelinter_setting == None: reload_settings(view) if view.is_scratch() or sublimelinter_setting == False: return reload_view_module(view) queue_linter(select_linter(view), view, preemptive=True, event='on_post_save') def on_selection_modified(self, view): if view.is_scratch(): return delay_queue(1000) # on movement, delay queue (to make movement responsive) # We only display errors in the status bar for the last line in the current selection. # If that line number has not changed, there is no point in updating the status bar. lastSelectedLineNo = last_selected_lineno(view) if lastSelectedLineNo != self.lastSelectedLineNo: self.lastSelectedLineNo = lastSelectedLineNo update_statusbar(view) class FindLintErrorCommand(sublime_plugin.TextCommand): '''This command is just a superclass for other commands, it is never enabled.''' def is_enabled(self): return select_linter(self.view) is not None def find_lint_error(self, forward): linter = select_linter(self.view, ignore_disabled=True) if not linter: return self.view.run_command('lint', linter.language) regions = get_lint_regions(self.view, reverse=not forward, coalesce=True) if len(regions) == 0: sublime.error_message('No lint errors.') return selected = self.view.sel() point = selected[0].begin() if forward else selected[-1].end() regionToSelect = None # If going forward, find the first region beginning after the point. # If going backward, find the first region ending before the point. # If nothing is found in the given direction, wrap to the first/last region. if forward: for index, region in enumerate(regions): if point < region.begin(): regionToSelect = region break else: for index, region in enumerate(regions): if point > region.end(): regionToSelect = region break # If there is only one error line and the cursor is in that line, we cannot move. # Otherwise wrap to the first/last error line unless settings disallow that. if regionToSelect is None and (len(regions) > 1 or not regions[0].contains(point)): if self.view.settings().get('sublimelinter_wrap_find', True): regionToSelect = regions[0] if regionToSelect is not None: select_lint_region(self.view, regionToSelect) else: sublime.error_message('No {0} lint errors.'.format('next' if forward else 'previous')) return regionToSelect class FindNextLintErrorCommand(FindLintErrorCommand): def run(self, edit): ''' Move the cursor to the next lint error in the current view. The search will wrap to the top unless the sublimelinter_wrap_find setting is set to false. ''' self.find_lint_error(forward=True) class FindPreviousLintErrorCommand(FindLintErrorCommand): def run(self, edit): ''' Move the cursor to the previous lint error in the current view. The search will wrap to the bottom unless the sublimelinter_wrap_find setting is set to false. ''' self.find_lint_error(forward=False) class SublimelinterWindowCommand(sublime_plugin.WindowCommand): def is_enabled(self): view = self.window.active_view() if view: if view.is_scratch(): return False else: return True else: return False def run_(self, args): pass class SublimelinterAnnotationsCommand(SublimelinterWindowCommand): '''Commands to extract annotations and display them in a file ''' def run_(self, args): linter = LINTERS.get('annotations', None) if linter is None: return view = self.window.active_view() if not view: return text = view.substr(sublime.Region(0, view.size())).encode('utf-8') filename = (view.file_name() or '').encode('utf-8') notes = linter.extract_annotations(text, view, filename) _, filename = os.path.split(filename) annotations_view, _id = view_in_tab(view, 'Annotations from {0}'.format(filename), notes, '') class SublimelinterCommand(SublimelinterWindowCommand): def is_enabled(self): enabled = super(SublimelinterCommand, self).is_enabled() if not enabled: return False linter = select_linter(self.window.active_view(), ignore_disabled=True) return linter is not None def run_(self, args={}): view = self.window.active_view() action = args.get('action', '') if view and action: if action == 'lint': self.lint_view(view, show_popup_list=args.get('show_popup', False)) else: view.run_command('lint', action) def lint_view(self, view, show_popup_list): linter = select_linter(view, ignore_disabled=True) if linter: view.run_command('lint', linter.language) regions = get_lint_regions(view, coalesce=True) if regions: if show_popup_list: popup_error_list(view) else: sublime.error_message('{0} lint error{1}.'.format(len(regions), 's' if len(regions) != 1 else '')) else: sublime.error_message('No lint errors.') else: syntax = syntax_name(view) sublime.error_message('No linter for the syntax "{0}"'.format(syntax)) class SublimelinterLintCommand(SublimelinterCommand): def is_enabled(self): enabled = super(SublimelinterLintCommand, self).is_enabled() if enabled: view = self.window.active_view() if view and view.settings().get('sublimelinter') == True: return False return enabled class SublimelinterShowErrorsCommand(SublimelinterCommand): def is_enabled(self): return super(SublimelinterShowErrorsCommand, self).is_enabled() class SublimelinterEnableLoadSaveCommand(SublimelinterCommand): def is_enabled(self): enabled = super(SublimelinterEnableLoadSaveCommand, self).is_enabled() if enabled: view = self.window.active_view() if view and view.settings().get('sublimelinter') == 'load-save': return False return enabled class SublimelinterEnableSaveOnlyCommand(SublimelinterCommand): def is_enabled(self): enabled = super(SublimelinterEnableSaveOnlyCommand, self).is_enabled() if enabled: view = self.window.active_view() if view and view.settings().get('sublimelinter') == 'save-only': return False return enabled class SublimelinterDisableCommand(SublimelinterCommand): def is_enabled(self): enabled = super(SublimelinterDisableCommand, self).is_enabled() if enabled: view = self.window.active_view() if view and view.settings().get('sublimelinter') == False: return False return enabled ================================================ FILE: SublimeLinter.sublime-settings ================================================ /* SublimeLinter default settings */ { /* Sets the mode in which SublimeLinter runs: true - Linting occurs in the background as you type. false - Linting only occurs when you initiate it. "load-save" - Linting occurs only when a file is loaded and saved (the default). "save-only" - Linting occurs only when a file is saved. */ "sublimelinter": "load-save", /* Maps language names **as listed at the beginning of the README** (but all lowercase) to executables for non-built in linters. If the executable is not in the default system path, or on posix systems is not in /usr/local/bin or ~/bin, then you must specify the full path to the executable. Note that paths in Windows must use double backslashes, for example "C:\\Program Files (x86)\\nodejs\\node.exe". Please note that the map _keys_ do not always match the name of the executable, but rather the language syntax for the executable to lint. This is the effective default map; your mappings may override these. "sublimelinter_executable_map": { "perl": "perl", "php": "php", "ruby": "ruby" }, */ "sublimelinter_executable_map": { }, /* Maps syntax names to linters. This allows variations on a syntax (for example "Python (Django)") to be linted. The key is the name of the syntax **as it appears in the syntax list at the bottom right of the window**, and the value is the linter name **as listed in the README** (all lowercase) that the syntax maps to. */ "sublimelinter_syntax_map": { "Python Django": "python", "Ruby on Rails": "ruby", "C++": "c" }, // An array of linter names to disable. Names should be lowercase. "sublimelinter_disable": [ ], /* The minimum delay in seconds (fractional seconds are okay) before a linter is run when the "sublimelinter" setting is true. This allows you to have background linting active, but defer the actual linting until you are idle. When this value is greater than the built in linting delay, errors are erased when the file is modified, since the assumption is you don't want to see errors while you type. */ "sublimelinter_delay": 2, /* Selects the way the lines with errors or warnings are marked; "outline" draws outline boxes around the lines, "fill" fills the lines with the outline color, and "none" (default) disables all outline styles. */ "sublimelinter_mark_style": "none", /* If true, lines with errors or warnings will be filled in with the outline color. This setting is DEPRECATED and will be ignored in future versions. Use "sublimelinter_mark_style" instead. For backwards compatibility reasons, this setting overrides "sublimelinter_mark_style" if that one is set to "outline", but has no effect if it's set to "none". */ "sublimelinter_fill_outlines": false, // If true, lines with errors or warnings will have a gutter mark. "sublimelinter_gutter_marks": true, /* Choose the theme for gutter marks; available built-in options are: "alpha", "bright", "dark", "hard" and "simple" Anything else will be treated as a path to a set of images. For instance, setting the value to "../User/my-awesome-theme" would cause SublimeLinter to look for the images: "../User/my-awesome-theme-illegal.png", "../User/my-awesome-theme-violation.png", "../User/my-awesome-theme-warning.png" These images should all be approximately 32x32px. */ "sublimelinter_gutter_marks_theme": "simple", // If true, the find next/previous error commands will wrap. "sublimelinter_wrap_find": true, // If true, when the file is saved any errors will appear in a popup list "sublimelinter_popup_errors_on_save": false, // JavaScript linter: "gjslint" to use the closure javascript linter (if available), // or either "jshint" or "jslint" to use a built in linter. "javascript_linter": "jshint", // jshint: options for linting JavaScript. See http://www.jshint.com/docs/#options for more info. // By deault, eval is allowed. "jshint_options": { // To fix column positions for JSHint errors you may want to add `"indent": 1` to your // **User** "jshint_options". This issue affects users with tabs for indentation. // This fix was reverted due to a conflict with using the `"white": true` option. // "indent": 1, "evil": true, "regexdash": true, "browser": true, "wsh": true, "trailing": true, "sub": true, "devel": true }, // A list of command line options to send to gjslint. --nobeep is always sent. "gjslint_options": [ ], // A list of gjslint error numbers to ignore. The list of error codes is here: // http://closure-linter.googlecode.com/svn/trunk/closure_linter/errors.py "gjslint_ignore": [ 110 // line too long ], // CSSLint options: // Each rule can have three values: error|warning|true|false // false => rule is disabled. // true => alias to 'error' // All rules are enabled by default. // Currently the only difference between warnings and errors is in the prefix of the message in the Sublime status bar. "csslint_options": { "adjoining-classes": "warning", "box-model": true, "box-sizing": "warning", "compatible-vendor-prefixes": "warning", "display-property-grouping": true, "duplicate-background-images": "warning", "duplicate-properties": true, "empty-rules": true, "errors": true, "fallback-colors": "warning", "floats": "warning", "font-faces": "warning", "font-sizes": "warning", "gradients": "warning", "ids": "warning", "import": "warning", "important": "warning", "known-properties": true, "outline-none": "warning", "overqualified-elements": "warning", "qualified-headings": "warning", "regex-selectors": "warning", "rules-count": "warning", "shorthand": "warning", "star-property-hack": "warning", "text-indent": "warning", "underscore-property-hack": "warning", "unique-headings": "warning", "universal-selector": "warning", "vendor-prefix": true, "zero-units": "warning" }, // Set this to false to turn pep8 checking off completely "pep8": true, /* A list of pep8 error numbers to ignore. By default "line too long" errors are ignored. The list of error codes is in this file: https://github.com/jcrocholl/pep8/blob/master/pep8.py. Search for "Ennn:", where nnn is a 3-digit number. */ "pep8_ignore": [ "E501" ], /* If you use SublimeLinter for pyflakes checks, you can ignore some of the "undefined name xxx" errors (comes in handy if you work with post-processors, globals/builtins available only at runtime, etc.). You can control what names will be ignored with the user setting "pyflakes_ignore". Example: "pyflakes_ignore": [ "some_custom_builtin_o_mine", "A_GLOBAL_CONSTANT" ], */ "pyflakes_ignore": [ ], /* Ordinarily pyflakes will issue a warning when 'from foo import *' is used, but it is ignored since the warning is not that helpful. If you want to see this warning, set this option to false. */ "pyflakes_ignore_import_*": true, /* Perl linter: "perl" to use the Perl language syntax check, or "perlcritic" to use Perl::Critic linting. Perl is now set to use "perlcritic" by default due to a vulnerability with blindly running `perl -c` on files with `BEGIN` or `CHECK` blocks. */ "perl_linter": "perlcritic", // Objective-J: if true, non-ascii characters are flagged as an error. "sublimelinter_objj_check_ascii": false, // Set to true to highlight annotations "sublimelinter_notes": false, // The set of annotation phrases to highlight "annotations": ["TODO", "README", "FIXME"] } ================================================ FILE: changelog.txt ================================================ SublimeLinter 1.5.1 changelog ============================= NEW FEATURES ------------ - SublimeLinter keeps its settings in its own settings file now: SublimeLinter.sublime-settings. You will need to copy your user settings to this file. To do so, follow these steps: 1. Select "Preferences->Settings - User" in one tab/window. The title of this tab should be "Preferences.sublime-settings". 2. Open another tab/window and select `Preferences->Package Settings->SublimeLinter->Settings - User`. The title of this window should be `SublimeLinter.sublime-settings`. 3. Copy/cut any of the following settings from Preferences.sublime-settings to SublimeLinter.sublime-settings: sublimelinter sublimelinter_executable_map sublimelinter_syntax_map sublimelinter_disable sublimelinter_delay sublimelinter_fill_outlines sublimelinter_gutter_marks sublimelinter_wrap_find sublimelinter_popup_errors_on_save javascript_linter jshint_options pep8_ignore pyflakes_ignore pyflakes_ignore_import_* sublimelinter_objj_check_ascii 4. Save SublimeLinter.sublime-settings. The changes may not take effect until you restart Sublime Text. When changes are made to the user SublimeLinter settings, they are immediately reloaded into every open view. Note that this will override any temporary changes you may have made to the settings in a given view. - The google closure JavaScript linter (gjslint) is now supported. (https://developers.google.com/closure/utilities/docs/linter_howto) There is a new setting, `javascript_linter`, which determines which linter to use, jshint or gjslinter. You may also customize gjslint behavior with the `gjslint_options` and `gjslint_ignore` settings. Please select `Preferences->Package Settings->SublimeLinter->Settings - Default` for more information on these settings. - The color theme names have been changed to avoid clashes with built in names. Old New --------------------- ----------------------------- sublimelinter. sublimelinter.outline. invalid. sublimelinter.underline. You will have to update your color themes accordingly. Please select `Preferences->Package Settings->SublimeLinter->README` and search for "Customizing colors" for more information. - When selecting an error from the popup error list, the view is centered on the error line. CHANGES/FIXES ------------- - The PHP error regex has been updated to work with PHP 5.3.8 on Mac OS X. - The popup error list will no longer choke on non-ASCII text. - Selecting an error from the popup error list no longer attempts to go directly to the point of an error as this could not be done reliably. It will jump to the first non-whitespace character of the error's line. - Go to next/previous error works correctly when an error line has no underlines. - If an exception is thrown by jshint (e.g. too many errors), the errors captured up to that point are displayed. - The built in jshint has been updated from the master jshint. - Fixed errors that would occur with the popup error list when there was more than error on a line. SublimeLinter 1.5.2 changelog ============================= CHANGES/FIXES ------------- - Fixed a problem with messages.json that prevented correct upgrading. IMPORTANT --------- Please check to see if you have multiple listings for `SublimeLinter` in `Preferences -> Package Settings`. If you do see 2 listings, please run `Package Control: Upgrade/Overwrite All Packages` from the Command Palette (`Tools -> Command Palette`). SublimeLinter 1.5.3 changelog ============================= CHANGES/FIXES ------------- - Annotations have been fixed. - Entries in "sublimelinter_syntax_map" take precedence over built in mappings. - Lint errors in PHP files will hopefully not be logged to the PHP log file. SublimeLinter 1.5.4 changelog ============================= CHANGES/FIXES ------------- - jshint.js has been updated to the latest master version. - [issue #128] An "unsafe" option has been added to jshint. If set true, any UTF-8 characters are allowed in the source. SublimeLinter 1.5.5 changelog ============================= CHANGES/FIXES ------------- - This change log is available from the SublimeLinter preferences menu. SublimeLinter 1.5.6 changelog ============================= CHANGES/FIXES ------------- - Fixed a problem with messages.json that prevented correct upgrading. IMPORTANT --------- Please check to see if you have multiple listings for `SublimeLinter` in `Preferences -> Package Settings`. If you do see 2 listings, please run `Package Control: Upgrade/Overwrite All Packages` from the Command Palette (`Tools -> Command Palette`). SublimeLinter 1.5.7 changelog ============================= CHANGES/FIXES ------------- - node.js is the preferred JavaScript engine on Mac OS X and will be used if it is installed. JavaScriptCore does not handle non-ASCII text correctly and you should install node.js if possible. - If you imported `BaseLinter.JSC_PATH`, please change your linter to use the `self.jsc_path()` method instead. JSC_PATH should no longer be considered public. SublimeLinter 1.6.0 changelog ============================= NEW FEATURES ------------ - Simpler abstraction of JavaScript engines for JS powered linters. To leverage a JS linter, include a "linter.js" file; this file should `require` the actual linter library file and export a `lint` function. The `lint` function should return a list of errors back to the python language handler file (via the `errors` parameter to the `parse_errors` method). Although "linter.js" should follow the Node.js api, the linter may also be run via JavaScriptCore on OS X if Node.js is not installed. In the case where JavaScriptCore is used, require + export are shimmed to keep things consistent. However, it is important not to assume that a full Node.js api is available. If you must know what JS engine you are using, you may check for `USING_JSC` to be set as `true` when JavaScriptCore is used. For examples of using the JS engines, see "csslint", "jslint", and "jshint" in "SublimeLinter/sublimelinter/modules/libs" and the respective python code of "css.py" and "javascript.py" in "SublimeLinter/sublimelinter/modules". - Douglas Crockford's [JSLint](http://jslint.com) JavaScript linter is now supported. To use JSLint set the "javascript_linter" setting to "jslint". You may also customize jslint behavior with the "jslint_options" setting. For more information about options available to JSLint, see http://jslint.com/lint.html. - The [CSSLint](http://csslint.net) CSS linter is now supported. By default all CSSLint settings are turned on. You may customize csslint behavior with the "csslint_options" setting. Please select "Preferences->Package Settings->SublimeLinter->Settings - Default" for more information on turning off or adjusting severity of tests. For more information about options available to CSSLint, see https://github.com/stubbornella/csslint/wiki/Rules. SublimeLinter 1.6.1 changelog ============================= CHANGES/FIXES ------------- - Fixed an issue (#141) with JSLint running in Node.js - Updated CSSLint, JSLint, JSHint to latest stable releases. - Added additional debugging output (in Sublime console) when errors occur running linters written in JavaScript. SublimeLinter 1.6.2 changelog ============================= CHANGES/FIXES ------------- - Replaced the default perl linter with Perl::Critic. The standard Perl syntax checker can still be invoked by switching the "perl_linter" setting to "perl". - Added a LICENSE file to define appropriate usage of SublimeLinter and its source. - Converted README back to markdown. IMPORTANT --------- Due to a vulnerability (issue #77) with the Perl linter, Perl syntax checking is no longer enabled by default. The default linter for Perl has been replaced by Perl::Critic. SublimeLinter 1.6.3 changelog ============================= NEW FEATURES ------------ - Support for `.jshintrc` files. If using JSHint, SublimeLinter will recursively search the directory tree (from the file location to the file-system root directory). This functionality is specified in the JSHint README. https://github.com/jshint/node-jshint/#within-your-projects-directory-tree CHANGES/FIXES ------------- - Fixed README reference in the menu. - Updated CoffeeScript module to be compatible with the updated coffee command in version 1.3. IMPORTANT --------- If you are using the CoffeeScript linting, please upgrade the installed coffee-script NPM module to 1.3 or greater. npm update -g coffee-script SublimeLinter 1.6.4 changelog ============================= IMPORTANT!! ----------- Please note that the SublimeLinter repository has moved to: https://github.com/SublimeLinter/SublimeLinter Issues and pull requests should be made there. NEW FEATURES ------------ - The Objective-J linter now catches spaces inside parentheses and dependent clauses on the same line as a control structure. CHANGES/FIXES ------------- - The README has been reorganized to hopefully be clearer. - More explicit Node.js installation instructions have been provided. - The "pep8" setting is now recognized in SublimeLinter's settings. - When a minimum delay is specified with the "sublimelinter_delay" setting, SublimeLinter will only lint the currently displayed file when the queued linters run. This allows you to avoid linting of files as they are selected in the choose file palette. SublimeLinter 1.6.5 changelog ============================= NEW FEATURES ------------ - Added a (Ruby) Haml linter based on `haml -c`. For more information about Haml, please see http://haml.info. - Added a simple Git commit message linter. This linter follows the rules as defined by http://tbaggery.com/2008/04/19/a-note-about-git-commit-messages.html CHANGES/FIXES ------------- - Updated several links to point to the SublimeLinter's new Github location. - "Ruby on Rails" syntax maps to "ruby" as part of the default settings. - Linter arguments are now consistently defined as arrays (instead of tuples). - Syntax map settings are no longer (sometimes) case-sensitive. SublimeLinter 1.6.6 changelog ============================= CHANGES/FIXES ------------- - JSHint now shows underlines at the appropriate character positions when using tabs for indentation. - Upgrading CSSLint to the latest version (v0.9.8). This adds support for the latest "Compatibility" options: "Disallow star hack" and "Disallow underscore hack". - Annotation highlighting is working again. - Git Commit Message linting now ignores `git --diff` output in messages. These lines are automatically generated and inserted when running `git commit -v`. SublimeLinter 1.6.7 changelog ============================= NEW FEATURES ------------ - Puppet linting is now supported via `puppet parser validate`. - Added an option for more granular control of outline decorations. Set the value of "sublimelinter_mark_style" to "outline", "fill", or "none" in the user settings. CHANGES/FIXES ------------- - Repaired the built-in CSS linter (CSSLint). This was broken with with the last update. - Added missing documentation for "save-only" linting in the settings file. - Adjusted ambiguous/misleading documentation for the "sublimelinter_executable_map" setting. SublimeLinter 1.6.8 changelog ============================= NEW FEATURES ------------ - HTML5 linting support via `tidy`. This linter will not run unless you have a version of tidy with HTML5 support. To use this linter, please see: https://github.com/w3c/tidy-html5 - XML linting via `xmllint`. CHANGES/FIXES ------------- - Made significant progress on issue (#181). However, SublimeLinter still throws with some linter types on Windows 7 when a user has non-ascii characters in the path (to the SL plugin). - Updated PEP8 to v1.1 - Updated Pyflakes to v0.5.0 - Updated JSHint to latest stable (r11). - Reverted a fix for accurate (JSHint) error column positions (when using tab indentation) due to a regression with the `"white": true` option. You may still manually fix error positions by setting `"indent": 1`. - Changed (the default) background linting delay to a more sane 2 seconds. This reduces memory usage, cpu processing, and visual noise while you are actively writing code. SublimeLinter 1.6.9 changelog ============================= NEW FEATURES ------------ - C/C++ lint via `cppcheck`. Also added alternative (hidden) support for `cpplint.py`. Please see README for more info. - Lua syntax check via `luac`. CHANGES/FIXES ------------- - Adding a 'beta' channel for SublimeLinter into Package Control. This branch will act as a more formal method for testing new features and fixes before a release. SublimeLinter 1.6.10 changelog ============================== CHANGES/FIXES ------------- - Puppet validation supports error output for Puppet v3.0+. - JSHint options now support the (proper) "globals" definition. - Lua syntax check no longer creates luac.out file clutter. - Clarified documentation for styling sublimelinter.annotations. SublimeLinter 1.6.11 changelog ============================== CHANGES/FIXES ------------- - Github (nodeload) zip url scheme changed. SublimeLinter 1.6.12 changelog ============================== CHANGES/FIXES ------------- - Cpplint no longer uses a temporary file - it leverages stdin instead. - Fixed issue #298: C linter throws "KeyError" SublimeLinter 1.6.13 changelog ============================== CHANGES/FIXES ------------- - Updated PEP8 to version v1.4 - Updated PyFlakes to v0.6.1 - Updated CSSLint to v0.9.10 - Update JSHint to v1.0.0 - Update JSLint to latest - Fixed issues with attempting to encode file name when using scratch space. SublimeLinter 1.7.0 changelog ============================= NEW FEATURES ------------ - Add "Quick Start" section to the README - New commands for the (Sublime Text) command palette to open the SublimeLinter preferences files (Default & User) - Ruby linting support via [ruby-lint](https://github.com/YorickPeterse/ruby-lint) - Haskell linting support via [hlint](http://community.haskell.org/~ndm/hlint/) - Add gutter mark themes. Add 5 new built-in themes: "alpha", "bright", "dark", "hard", and "simple". Custom themes can also be used. CHANGES/FIXES ------------- - Update JSHint to v1.1.0 - Update CoffeeScript linter to support latest error message format - Settings are refreshed when a new file is saved; this should fix several bugs: #233, #359, #149, #367 - The `lint_args` setting now matches the documentation; it will override the default arguments rather than extend them. This will give users full control over linter/checker arguments. - Clean up markdown formatting in changelog IMPORTANT --------- Due to the fix for `lint_args`, if you are currently customizing this setting, you **must** adjust your settings. SublimeLinter 1.7.1 changelog ============================= CHANGES/FIXES ------------- - Update JSLint to latest - Update JSHint to v2.1.5 - Updated link to JSHint documentation - Updated PEP8 to v1.4.6 - Updated Pyflakes to v0.7.3 - Gutter mark icons are now retina quality - Included Gutter mark icon PSD (for tinkering :) SublimeLinter 1.7.2 changelog ============================= CHANGES/FIXES ------------- - Update JSHint to v2.1.8 - Fixing compatibility regression with latest JSHint - Fixing compatibility regression with latest Pyflakes - Apologizing for hasty v1.7.1 release ;) ================================================ FILE: messages/1.5.1.txt ================================================ SublimeLinter 1.5.1 changelog ============================= NEW FEATURES ------------ - SublimeLinter keeps its settings in its own settings file now: SublimeLinter.sublime-settings. You will need to copy your user settings to this file. To do so, follow these steps: 1. Select "Preferences->Settings - User" in one tab/window. The title of this tab should be "Preferences.sublime-settings". 2. Open another tab/window and select `Preferences->Package Settings->SublimeLinter->Settings - User`. The title of this window should be `SublimeLinter.sublime-settings`. 3. Copy/cut any of the following settings from Preferences.sublime-settings to SublimeLinter.sublime-settings: sublimelinter sublimelinter_executable_map sublimelinter_syntax_map sublimelinter_disable sublimelinter_delay sublimelinter_fill_outlines sublimelinter_gutter_marks sublimelinter_wrap_find sublimelinter_popup_errors_on_save javascript_linter jshint_options pep8_ignore pyflakes_ignore pyflakes_ignore_import_* sublimelinter_objj_check_ascii 4. Save SublimeLinter.sublime-settings. The changes may not take effect until you restart Sublime Text. When changes are made to the user SublimeLinter settings, they are immediately reloaded into every open view. Note that this will override any temporary changes you may have made to the settings in a given view. - The google closure JavaScript linter (gjslint) is now supported. (https://developers.google.com/closure/utilities/docs/linter_howto) There is a new setting, `javascript_linter`, which determines which linter to use, jshint or gjslinter. You may also customize gjslint behavior with the `gjslint_options` and `gjslint_ignore` settings. Please select `Preferences->Package Settings->SublimeLinter->Settings - Default` for more information on these settings. - The color theme names have been changed to avoid clashes with built in names. Old New --------------------- ----------------------------- sublimelinter. sublimelinter.outline. invalid. sublimelinter.underline. You will have to update your color themes accordingly. Please select `Preferences->Package Settings->SublimeLinter->README` and search for "Customizing colors" for more information. - When selecting an error from the popup error list, the view is centered on the error line. CHANGES/FIXES ------------- - The PHP error regex has been updated to work with PHP 5.3.8 on Mac OS X. - The popup error list will no longer choke on non-ASCII text. - Selecting an error from the popup error list no longer attempts to go directly to the point of an error as this could not be done reliably. It will jump to the first non-whitespace character of the error's line. - Go to next/previous error works correctly when an error line has no underlines. - If an exception is thrown by jshint (e.g. too many errors), the errors captured up to that point are displayed. - The built in jshint has been updated from the master jshint. - Fixed errors that would occur with the popup error list when there was more than error on a line. ================================================ FILE: messages/1.5.2.txt ================================================ SublimeLinter 1.5.2 changelog ============================= CHANGES/FIXES ------------- - Fixed a problem with messages.json that prevented correct upgrading. IMPORTANT --------- Please check to see if you have multiple listings for `SublimeLinter` in `Preferences -> Package Settings`. If you do see 2 listings, please run `Package Control: Upgrade/Overwrite All Packages` from the Command Palette (`Tools -> Command Palette`). ================================================ FILE: messages/1.5.3.txt ================================================ SublimeLinter 1.5.3 changelog ============================= CHANGES/FIXES ------------- - Annotations have been fixed. - Entries in "sublimelinter_syntax_map" take precedence over built in mappings. - Lint errors in PHP files will hopefully not be logged to the PHP log file. ================================================ FILE: messages/1.5.4.txt ================================================ SublimeLinter 1.5.4 changelog ============================= CHANGES/FIXES ------------- - jshint.js has been updated to the latest master version. - [issue #128] An "unsafe" option has been added to jshint. If set true, any UTF-8 characters are allowed in the source. ================================================ FILE: messages/1.5.5.txt ================================================ SublimeLinter 1.5.5 changelog ============================= CHANGES/FIXES ------------- - This change log is available from the SublimeLinter preferences menu. ================================================ FILE: messages/1.5.6.txt ================================================ SublimeLinter 1.5.6 changelog ============================= CHANGES/FIXES ------------- - Fixed a problem with messages.json that prevented correct upgrading. IMPORTANT --------- Please check to see if you have multiple listings for `SublimeLinter` in `Preferences -> Package Settings`. If you do see 2 listings, please run `Package Control: Upgrade/Overwrite All Packages` from the Command Palette (`Tools -> Command Palette`). ================================================ FILE: messages/1.5.7.txt ================================================ SublimeLinter 1.5.7 changelog ============================= CHANGES/FIXES ------------- - node.js is the preferred JavaScript engine on Mac OS X and will be used if it is installed. JavaScriptCore does not handle non-ASCII text correctly and you should install node.js if possible. - If you imported `BaseLinter.JSC_PATH`, please change your linter to use the `self.jsc_path()` method instead. JSC_PATH should no longer be considered public. ================================================ FILE: messages/1.6.0.txt ================================================ SublimeLinter 1.6.0 changelog ============================= NEW FEATURES ------------ - Simpler abstraction of JavaScript engines for JS powered linters. To leverage a JS linter, include a "linter.js" file; this file should `require` the actual linter library file and export a `lint` function. The `lint` function should return a list of errors back to the python language handler file (via the `errors` parameter to the `parse_errors` method). Although "linter.js" should follow the Node.js api, the linter may also be run via JavaScriptCore on OS X if Node.js is not installed. In the case where JavaScriptCore is used, require + export are shimmed to keep things consistent. However, it is important not to assume that a full Node.js api is available. If you must know what JS engine you are using, you may check for `USING_JSC` to be set as `true` when JavaScriptCore is used. For examples of using the JS engines, see "csslint", "jslint", and "jshint" in "SublimeLinter/sublimelinter/modules/libs" and the respective python code of "css.py" and "javascript.py" in "SublimeLinter/sublimelinter/modules". - Douglas Crockford's [JSLint](http://jslint.com) JavaScript linter is now supported. To use JSLint set the "javascript_linter" setting to "jslint". You may also customize jslint behavior with the "jslint_options" setting. For more information about options available to JSLint, see http://jslint.com/lint.html. - The [CSSLint](http://csslint.net) CSS linter is now supported. By default all CSSLint settings are turned on. You may customize csslint behavior with the "csslint_options" setting. Please select "Preferences->Package Settings->SublimeLinter->Settings - Default" for more information on turning off or adjusting severity of tests. For more information about options available to CSSLint, see https://github.com/stubbornella/csslint/wiki/Rules. ================================================ FILE: messages/1.6.1.txt ================================================ SublimeLinter 1.6.1 changelog ============================= CHANGES/FIXES ------------- - Fixed an issue (#141) with JSLint running in Node.js - Updated CSSLint, JSLint, JSHint to latest stable releases. - Added additional debugging output (in Sublime console) when errors occur running linters written in JavaScript. ================================================ FILE: messages/1.6.10.txt ================================================ SublimeLinter 1.6.10 changelog ============================== CHANGES/FIXES ------------- - Puppet validation supports error output for Puppet v3.0+. - JSHint options now support the (proper) "globals" definition. - Lua syntax check no longer creates luac.out file clutter. - Clarified documentation for styling sublimelinter.annotations. ================================================ FILE: messages/1.6.11.txt ================================================ SublimeLinter 1.6.11 changelog ============================== CHANGES/FIXES ------------- - Github (nodeload) zip url scheme changed. ================================================ FILE: messages/1.6.12.txt ================================================ SublimeLinter 1.6.12 changelog ============================== CHANGES/FIXES ------------- - Cpplint no longer uses a temporary file - it leverages stdin instead. - Fixed issue #298: C linter throws "KeyError" ================================================ FILE: messages/1.6.13.txt ================================================ SublimeLinter 1.6.13 changelog ============================== CHANGES/FIXES ------------- - Updated PEP8 to version v1.4 - Updated PyFlakes to v0.6.1 - Updated CSSLint to v0.9.10 - Update JSHint to v1.0.0 - Update JSLint to latest - Fixed issues with attempting to encode file name when using scratch space. ================================================ FILE: messages/1.6.2.txt ================================================ SublimeLinter 1.6.2 changelog ============================= CHANGES/FIXES ------------- - Replaced the default perl linter with Perl::Critic. The standard Perl syntax checker can still be invoked by switching the "perl_linter" setting to "perl". - Added a LICENSE file to define appropriate usage of SublimeLinter and its source. - Converted README back to markdown. IMPORTANT --------- Due to a vulnerability (issue #77) with the Perl linter, Perl syntax checking is no longer enabled by default. The default linter for Perl has been replaced by Perl::Critic. ================================================ FILE: messages/1.6.3.txt ================================================ SublimeLinter 1.6.3 changelog ============================= NEW FEATURES ------------ - Support for `.jshintrc` files. If using JSHint, SublimeLinter will recursively search the directory tree (from the file location to the file-system root directory). This functionality is specified in the JSHint README. https://github.com/jshint/node-jshint/#within-your-projects-directory-tree CHANGES/FIXES ------------- - Fixed README reference in the menu. - Updated CoffeeScript module to be compatible with the updated coffee command in version 1.3. IMPORTANT --------- If you are using the CoffeeScript linting, please upgrade the installed coffee-script NPM module to 1.3 or greater. npm update -g coffee-script ================================================ FILE: messages/1.6.4.txt ================================================ SublimeLinter 1.6.4 changelog ============================= IMPORTANT!! ----------- Please note that the SublimeLinter repository has moved to: https://github.com/SublimeLinter/SublimeLinter Issues and pull requests should be made there. NEW FEATURES ------------ - The Objective-J linter now catches spaces inside parentheses and dependent clauses on the same line as a control structure. CHANGES/FIXES ------------- - The README has been reorganized to hopefully be clearer. - More explicit Node.js installation instructions have been provided. - The "pep8" setting is now recognized in SublimeLinter's settings. - When a minimum delay is specified with the "sublimelinter_delay" setting, SublimeLinter will only lint the currently displayed file when the queued linters run. This allows you to avoid linting of files as they are selected in the choose file palette. ================================================ FILE: messages/1.6.5.txt ================================================ SublimeLinter 1.6.5 changelog ============================= NEW FEATURES ------------ - Added a (Ruby) Haml linter based on `haml -c`. For more information about Haml, please see http://haml.info. - Added a simple Git commit message linter. This linter follows the rules as defined by http://tbaggery.com/2008/04/19/a-note-about-git-commit-messages.html CHANGES/FIXES ------------- - Updated several links to point to the SublimeLinter's new Github location. - "Ruby on Rails" syntax maps to "ruby" as part of the default settings. - Linter arguments are now consistently defined as arrays (instead of tuples). - Syntax map settings are no longer (sometimes) case-sensitive. ================================================ FILE: messages/1.6.6.txt ================================================ SublimeLinter 1.6.6 changelog ============================= CHANGES/FIXES ------------- - JSHint now shows underlines at the appropriate character positions when using tabs for indentation. - Upgrading CSSLint to the latest version (v0.9.8). This adds support for the latest "Compatibility" options: "Disallow star hack" and "Disallow underscore hack". - Annotation highlighting is working again. - Git Commit Message linting now ignores `git --diff` output in messages. These lines are automatically generated and inserted when running `git commit -v`. ================================================ FILE: messages/1.6.7.txt ================================================ SublimeLinter 1.6.7 changelog ============================= NEW FEATURES ------------ - Puppet linting is now supported via `puppet parser validate`. - Added an option for more granular control of outline decorations. Set the value of "sublimelinter_mark_style" to "outline", "fill", or "none" in the user settings. CHANGES/FIXES ------------- - Repaired the built-in CSS linter (CSSLint). This was broken with with the last update. - Added missing documentation for "save-only" linting in the settings file. - Adjusted ambiguous/misleading documentation for the "sublimelinter_executable_map" setting. ================================================ FILE: messages/1.6.8.txt ================================================ SublimeLinter 1.6.8 changelog ============================= NEW FEATURES ------------ - HTML5 linting support via `tidy`. This linter will not run unless you have a version of tidy with HTML5 support. To use this linter, please see: https://github.com/w3c/tidy-html5 - XML linting via `xmllint`. CHANGES/FIXES ------------- - Made significant progress on issue (#181). However, SublimeLinter still throws with some linter types on Windows 7 when a user has non-ascii characters in the path (to the SL plugin). - Updated PEP8 to v1.1 - Updated Pyflakes to v0.5.0 - Updated JSHint to latest stable (r11). - Reverted a fix for accurate (JSHint) error column positions (when using tab indentation) due to a regression with the `"white": true` option. You may still manually fix error positions by setting `"indent": 1`. - Changed (the default) background linting delay to a more sane 2 seconds. This reduces memory usage, cpu processing, and visual noise while you are actively writing code. ================================================ FILE: messages/1.6.9.txt ================================================ SublimeLinter 1.6.9 changelog ============================= NEW FEATURES ------------ - C/C++ lint via `cppcheck`. Also added alternative (hidden) support for `cpplint.py`. Please see README for more info. - Lua syntax check via `luac`. CHANGES/FIXES ------------- - Adding a 'beta' channel for SublimeLinter into Package Control. This branch will act as a more formal method for testing new features and fixes before a release. ================================================ FILE: messages/1.7.0.txt ================================================ SublimeLinter 1.7.0 changelog ============================= NEW FEATURES ------------ - Add "Quick Start" section to the README - New commands for the (Sublime Text) command palette to open the SublimeLinter preferences files (Default & User) - Ruby linting support via [ruby-lint](https://github.com/YorickPeterse/ruby-lint) - Haskell linting support via [hlint](http://community.haskell.org/~ndm/hlint/) - Add gutter mark themes. Add 5 new built-in themes: "alpha", "bright", "dark", "hard", and "simple". Custom themes can also be used. CHANGES/FIXES ------------- - Update JSHint to v1.1.0 - Update CoffeeScript linter to support latest error message format - Settings are refreshed when a new file is saved; this should fix several bugs: #233, #359, #149, #367 - The `lint_args` setting now matches the documentation; it will override the default arguments rather than extend them. This will give users full control over linter/checker arguments. - Clean up markdown formatting in changelog IMPORTANT --------- Due to the fix for `lint_args`, if you are currently customizing this setting, you **must** adjust your settings. ================================================ FILE: messages/1.7.1.txt ================================================ SublimeLinter 1.7.1 changelog ============================= CHANGES/FIXES ------------- - Update JSLint to latest - Update JSHint to v2.1.5 - Updated link to JSHint documentation - Updated PEP8 to v1.4.6 - Updated Pyflakes to v0.7.3 - Gutter mark icons are now retina quality - Included Gutter mark icon PSD (for tinkering :) ================================================ FILE: messages/1.7.2.txt ================================================ SublimeLinter 1.7.2 changelog ============================= CHANGES/FIXES ------------- - Update JSHint to v2.1.8 - Fixing compatibility regression with latest JSHint - Fixing compatibility regression with latest Pyflakes - Apologizing for hasty v1.7.1 release ;) ================================================ FILE: messages/SublimeLinter3-update1.txt ================================================ ===================== SublimeLinter for ST3 Fundraising update ===================== As of October 28, we have raised $980 (after fees) from 90 donations. This is enough to get started, but I was hoping for twice that amount. If you are one of the thousands of happy SublimeLinter users who has not donated, please consider making a small donation to keep this plugin alive. Open source software is not free! It is we the developers who usually end up paying for it. To donate and find out more about what you can expect from SublimeLinter3, go here: https://github.com/SublimeLinter/SublimeLinter3#sublimelinter3 If you are interested in helping with development, please leave a message here: https://github.com/SublimeLinter/SublimeLinter3/issues/3 Thanks again for your support, - Aparajita Fishman SublimeLinter maintainer ================================================ FILE: messages/SublimeLinter3-update2.txt ================================================ ===================== SublimeLinter for ST3 Fundraising update October 31, 2013 ===================== Through October 30, we have raised $2885 (after fees) from 226 donations. Thank you for your support! Development has started, and I’m very excited about this new version! But there is still a lot of work to do, and I’m getting cool new ideas every day. So if you are one of the thousands of happy SublimeLinter users who has not donated yet, it isn’t too late to contribute. Please consider making a small donation to fund ongoing development and to compensate Ryan Hileman for the great work on which SublimeLinter3 is based. Open source software is not free! It is we the developers who usually end up paying for it. To donate and find out more about what you can expect from SublimeLinter3, go here: https://github.com/SublimeLinter/SublimeLinter3#sublimelinter3 If you are interested in helping with development, please leave a message here: https://github.com/SublimeLinter/SublimeLinter3/issues/3 Thanks again for your support, - Aparajita Fishman SublimeLinter maintainer ================================================ FILE: messages/SublimeLinter3-update3.txt ================================================ ____ _ _ _ _ _ _ _____ / ___| _ _| |__ | (_)_ __ ___ ___| | (_)_ __ | |_ ___ _ __ |___ / \___ \| | | | '_ \| | | '_ ` _ \ / _ \ | | | '_ \| __/ _ \ '__| |_ \ ___) | |_| | |_) | | | | | | | | __/ |___| | | | | || __/ | ___) | |____/ \__,_|_.__/|_|_|_| |_| |_|\___|_____|_|_| |_|\__\___|_| |____/ _ | |__ __ _ ___ | '_ \ / _` / __| | | | | (_| \__ \ |_| |_|\__,_|___/ _ _ _ _ | | __ _ _ __ __| | ___ __| | | | |/ _` | '_ \ / _` |/ _ \/ _` | | | | (_| | | | | (_| | __/ (_| |_| |_|\__,_|_| |_|\__,_|\___|\__,_(_) SublimeLinter for Sublime Text 3 is here, and it’s soooooo much better than before! Install it from Package Control and enjoy! Unless someone else comes forward, SublimeLinter for Sublime Text 2 will no longer be supported. I strongly encourage everyone to upgrade to Sublime Text 3 and SublimeLinter 3 — you’ll be glad you did! Take a look at the extensive documentation to see the great new features in SublimeLinter 3. https://github.com/SublimeLinter/SublimeLinter3 ***************** Share the love! ***************** I spent hundreds of hours writing and documenting SublimeLinter 3 to make it the best it can be — easy to use, easy to configure, easy to update, easy to extend. If you use SublimeLinter and feel it is making your coding life better and easier, please consider making a donation to help fund development and support. Thank you! To donate: https://github.com/SublimeLinter/SublimeLinter3#share-the-love Thank you for your support! - Aparajita Fishman SublimeLinter maintainer ================================================ FILE: messages/SublimeLinter3.txt ================================================ ===================== SublimeLinter for ST3 We need your help! ===================== According to the Package Control site, SublimeLinter is the 4th most popular plugin for Sublime Text, with 224K installs. That's an incredible number. Thank you to everyone! We all want SublimeLinter to become fully compatible with Sublime Text 3, and we want it to be better supported. In moving to ST3 (and python 3), we decided to start from scratch with a new architecture (based on sublimelint) that allows us to get the most out of both ST3 and python 3. And we plan to join forces with Ryan Hileman and merge sublimelint and SublimeLinter, so we can pool our resources. To find out more about what you can expect from SublimeLinter3, read about it here: https://github.com/SublimeLinter/SublimeLinter3#sublimelinter3 To make this happen any time soon, we need your help. I have lots of other projects that currently have a higher priority than SublimeLinter3. If you value SublimeLinter and want to see it running on ST3 sooner rather than later, please consider donating. If we can raise $3-5K, I'll move SublimeLinter3 to top of my priority list and finish it off within a week or two. See the link above for info on donating. Thanks again, - Aparajita Fishman SublimeLinter maintainer ================================================ FILE: messages/install.txt ================================================ SublimeLinter ============= SublimeLinter is a plugin that supports "lint" programs (known as "linters"). SublimeLinter highlights lines of code the linter deems to contain (potential) errors. It also supports highlighting special annotations (for example: TODO) so that they can be quickly located. SublimeLinter has built in linters for the following languages: * C/C++ - lint via `cppcheck` * CoffeeScript - lint via `coffee -s -l` * CSS - lint via built-in [csslint](http://csslint.net) * Git Commit Messages - lint via built-in module based on [A Note About Git Commit Messages](http://tbaggery.com/2008/04/19/a-note-about-git-commit-messages.html). * Haml - syntax check via `haml -c` * HTML - lint via `tidy` (actually [tidy for HTML5](http://w3c.github.com/tidy-html5/)) * Java - lint via `javac -Xlint` * JavaScript - lint via built in [jshint](http://jshint.org), [jslint](http://jslint.com), or the [closure linter (gjslint)](https://developers.google.com/closure/utilities/docs/linter_howto) (if installed) * Lua - syntax check via `luac` * Objective-J - lint via built-in [capp_lint](https://github.com/aparajita/capp_lint) * Perl - lint via [Perl:Critic](http://perlcritic.com/) or syntax+deprecation check via `perl -c` * PHP - syntax check via `php -l` * Puppet - syntax check via `puppet parser validate` * Python - native, moderately-complete lint * Ruby - syntax check via `ruby -wc` * XML - lint via `xmllint` For more information: --------------------- Please take the time to read the documentation: * Online - https://github.com/SublimeLinter/SublimeLinter * Sublime Text - Select Preferences->Package Settings->SublimeLinter->README IMPORTANT Do NOT edit the default SublimeLinter settings. Your changes will be lost when SublimeLinter is updated. ALWAYS edit the user SublimeLinter settings by selecting "Preferences->Package Settings->SublimeLinter->Settings - User". Note that individual settings you include in your user settings will **completely** replace the corresponding default setting, so you must provide that setting in its entirety. ================================================ FILE: messages.json ================================================ { "install": "messages/install.txt", "1.5.1": "messages/1.5.1.txt", "1.5.2": "messages/1.5.2.txt", "1.5.3": "messages/1.5.3.txt", "1.5.4": "messages/1.5.4.txt", "1.5.5": "messages/1.5.5.txt", "1.5.6": "messages/1.5.6.txt", "1.5.7": "messages/1.5.7.txt", "1.6.0": "messages/1.6.0.txt", "1.6.1": "messages/1.6.1.txt", "1.6.2": "messages/1.6.2.txt", "1.6.3": "messages/1.6.3.txt", "1.6.4": "messages/1.6.4.txt", "1.6.5": "messages/1.6.5.txt", "1.6.6": "messages/1.6.6.txt", "1.6.7": "messages/1.6.7.txt", "1.6.8": "messages/1.6.8.txt", "1.6.9": "messages/1.6.9.txt", "1.6.10": "messages/1.6.10.txt", "1.6.11": "messages/1.6.11.txt", "1.6.12": "messages/1.6.12.txt", "1.6.13": "messages/1.6.13.txt", "1.7.0": "messages/1.7.0.txt", "1.7.1": "messages/1.7.1.txt", "1.7.2": "messages/1.7.2.txt", "1.7.2+1": "messages/SublimeLinter3.txt", "1.7.2+3": "messages/SublimeLinter3-update1.txt", "1.7.2+4": "messages/SublimeLinter3-update2.txt", "1.7.2+5": "messages/SublimeLinter3-update3.txt" } ================================================ FILE: package_control.json ================================================ { "schema_version": "1.1", "packages": [ { "name": "SublimeLinter", "description": "Inline lint highlighting for the Sublime Text 2 editor", "author": "Kronuz, Aparajita Fishman, Jake Swartwood", "homepage": "http://github.com/SublimeLinter/SublimeLinter", "platforms": { "*": [ { "version": "1.7.2+4", "url": "https://nodeload.github.com/SublimeLinter/SublimeLinter/zip/v1.7.2+4" } ] } }, { "name": "SublimeLinter Beta", "description": "This version is considered unstable; only install this package if you plan on testing", "author": "Kronuz, Aparajita Fishman, Jake Swartwood", "homepage": "https://github.com/SublimeLinter/SublimeLinter/tree/beta", "platforms": { "*": [ { "version": "1.7.3-beta.0", "url": "https://nodeload.github.com/SublimeLinter/SublimeLinter/zip/beta" } ] } } ] } ================================================ FILE: sublimelinter/__init__.py ================================================ ================================================ FILE: sublimelinter/loader.py ================================================ # Note: Unlike linter modules, changes made to this module will NOT take effect until # Sublime Text is restarted. import glob import os import os.path import sys import modules.base_linter as base_linter # sys.path appears to ignore individual paths with unicode characters. # This means that this lib_path will be ignored for Windows 7 users with # non-ascii characters in their username (thus as their home directory). # # libs_path = os.path.abspath(os.path.join(os.path.dirname(__file__.encode('utf-8')), u'modules', u'libs')) # # if libs_path not in sys.path: # sys.path.insert(0, libs_path) # As a fix for the Windows 7 lib path issue (#181), the individual modules in # the `libs` folder can be explicitly imported. This obviously doesn't scale # well, but may be a necessary evil until ST2 upgrades its internal Python. # tmpdir = os.getcwdu() os.chdir(os.path.abspath(os.path.join(os.path.dirname(__file__.encode('utf-8')), u'modules', u'libs'))) for mod in [u'capp_lint', u'pep8', u'pyflakes', u'pyflakes.api', u'pyflakes.checker', u'pyflakes.messages', u'pyflakes.reporter']: __import__(mod) print u'imported {0}'.format(mod) os.chdir(tmpdir) class Loader(object): '''utility class to load (and reload if necessary) SublimeLinter modules''' def __init__(self, basedir, linters): '''assign relevant variables and load all existing linter modules''' self.basedir = basedir self.basepath = u'sublimelinter/modules' self.linters = linters self.modpath = self.basepath.replace('/', u'.') self.ignored = ('__init__', 'base_linter') self.fix_path() self.load_all() def fix_path(self): if os.name != 'posix': return path = os.environ['PATH'].encode('utf-8') home_path = os.path.join(os.path.expanduser(u'~'), u'bin') if path: dirs = path.encode('utf-8').split(':') if u'/usr/local/bin' not in dirs: dirs.insert(0, u'/usr/local/bin') if home_path not in dirs: dirs.append(home_path) os.environ['PATH'] = u':'.join(dirs) def load_all(self): '''loads all existing linter modules''' for modf in glob.glob(u'{0}/*.py'.format(self.basepath)): base, name = os.path.split(modf) name = name.split('.', 1)[0] if name in self.ignored: continue self.load_module(name) def load_module(self, name): '''loads a single linter module''' fullmod = u'{0}.{1}'.format(self.modpath, name) # make sure the path didn't change on us (this is needed for submodule reload) pushd = os.getcwdu() os.chdir(self.basedir) __import__(fullmod) # this following line of code does two things: # first, we get the actual module from sys.modules, # not the base mod returned by __import__ # second, we get an updated version with reload() # so module development is easier # (to make sublime text reload language submodule, # just save sublimelinter_plugin.py ) mod = sys.modules[fullmod] = reload(sys.modules[fullmod]) # update module's __file__ to absolute path so we can reload it # if saved with sublime text mod.__file__ = os.path.abspath(mod.__file__.encode('utf-8')).rstrip('co') language = '' try: config = base_linter.CONFIG.copy() try: config.update(mod.CONFIG) language = config['language'] except (AttributeError, KeyError): pass if language: if hasattr(mod, 'Linter'): linter = mod.Linter(config) else: linter = base_linter.BaseLinter(config) lc_language = language.lower() self.linters[lc_language] = linter print u'SublimeLinter: {0} loaded'.format(language) else: print u'SublimeLinter: {0} disabled (no language specified in module)'.format(name) except KeyError: print u'SublimeLinter: general error importing {0} ({1})'.format(name, language or '') os.chdir(pushd) def reload_module(self, module): '''reload a single linter module This method is meant to be used when editing a given linter module so that changes can be viewed immediately upon saving without having to restart Sublime Text''' fullmod = module.__name__ if not fullmod.startswith(self.modpath): return name = fullmod.replace(self.modpath + '.', '', 1) self.load_module(name) ================================================ FILE: sublimelinter/modules/__init__.py ================================================ ================================================ FILE: sublimelinter/modules/base_linter.py ================================================ # base_linter.py - base class for linters import os import os.path import json import re import subprocess import sublime # If the linter uses an executable that takes stdin, use this input method. INPUT_METHOD_STDIN = 1 # If the linter uses an executable that does not take stdin but you wish to use # a temp file so that the current view can be linted interactively, use this input method. # If the current view has been saved, the tempfile will have the same name as the # view's file, which is necessary for some linters. INPUT_METHOD_TEMP_FILE = 2 # If the linter uses an executable that does not take stdin and you wish to have # linting occur only on file load and save, use this input method. INPUT_METHOD_FILE = 3 CONFIG = { # The display language name for this linter. 'language': '', # Linters may either use built in code or use an external executable. This item may have # one of the following values: # # string - An external command (or path to a command) to execute # None - The linter is considered to be built in # # Alternately, your linter class may define the method get_executable(), # which should return the three-tuple (, , ): # must be a boolean than indicates whether the executable is available and usable. # If is True, must be one of: # - A command string (or path to a command) if an external executable will be used # - None if built in code will be used # - False if no suitable executable can be found or the linter should be disabled # for some other reason. # is the message that will be shown in the console when the linter is # loaded, to aid the user in knowing what the status of the linter is. If None or an empty string, # a default message will be returned based on the value of . Otherwise it # must be a string. 'executable': None, # If an external executable is being used, this item specifies the arguments # used when checking the existence of the executable to determine if the linter can be enabled. # If more than one argument needs to be passed, use a tuple/list. # Defaults to '-v' if this item is missing. 'test_existence_args': '-v', # If an external executable is being used, this item specifies the arguments to be passed # when linting. If there is more than one argument, use a tuple/list. # If the input method is anything other than INPUT_METHOD_STDIN, put a {filename} placeholder in # the args where the filename should go. # # Alternately, if your linter class may define the method get_lint_args(), which should return # None for no arguments or a tuple/list for one or more arguments. 'lint_args': None, # If an external executable is being used, the method used to pass input to it. Defaults to STDIN. 'input_method': INPUT_METHOD_STDIN } TEMPFILES_DIR = os.path.abspath(os.path.join(os.path.dirname(__file__.encode('utf-8')), u'..', u'.tempfiles')) JSON_MULTILINE_COMMENT_RE = re.compile(r'\/\*[\s\S]*?\*\/') JSON_SINGLELINE_COMMENT_RE = re.compile(r'\/\/[^\n\r]*') if not os.path.exists(TEMPFILES_DIR): os.mkdir(TEMPFILES_DIR) class BaseLinter(object): '''A base class for linters. Your linter module needs to do the following: - Set the relevant values in CONFIG - Override built_in_check() if it uses a built in linter. You may return whatever value you want, this value will be passed to parse_errors(). - Override parse_errors() and populate the relevant lists/dicts. The errors argument passed to parse_errors() is the output of the executable run through strip(). If you do subclass and override __init__, be sure to call super(MyLinter, self).__init__(config). ''' JSC_PATH = '/System/Library/Frameworks/JavaScriptCore.framework/Versions/A/Resources/jsc' LIB_PATH = os.path.abspath(os.path.join(os.path.dirname(__file__.encode('utf-8')), u'libs')) JAVASCRIPT_ENGINES = ['node', 'jsc'] JAVASCRIPT_ENGINE_NAMES = {'node': 'node.js', 'jsc': 'JavaScriptCore'} JAVASCRIPT_ENGINE_WRAPPERS_PATH = os.path.join(LIB_PATH, 'jsengines') def __init__(self, config): self.language = config['language'] self.enabled = False self.executable = config.get('executable', None) self.test_existence_args = config.get('test_existence_args', ['-v']) self.js_engine = None if isinstance(self.test_existence_args, basestring): self.test_existence_args = (self.test_existence_args,) self.input_method = config.get('input_method', INPUT_METHOD_STDIN) self.filename = None self.lint_args = config.get('lint_args', []) if isinstance(self.lint_args, basestring): self.lint_args = [self.lint_args] def check_enabled(self, view): if hasattr(self, 'get_executable'): try: self.enabled, self.executable, message = self.get_executable(view) if self.enabled and not message: message = 'using "{0}"'.format(self.executable) if self.executable else 'built in' except Exception as ex: self.enabled = False message = unicode(ex) else: self.enabled, message = self._check_enabled(view) return (self.enabled, message or '') def _check_enabled(self, view): if self.executable is None: return (True, 'built in') elif isinstance(self.executable, basestring): self.executable = self.get_mapped_executable(view, self.executable) elif isinstance(self.executable, bool) and self.executable is False: return (False, 'unknown error') else: return (False, 'bad type for CONFIG["executable"]') # If we get this far, the executable is external. Test that it can be executed # and capture stdout and stderr so they don't end up in the system log. try: args = [self.executable] args.extend(self.test_existence_args) subprocess.Popen(args, startupinfo=self.get_startupinfo(), stdout=subprocess.PIPE, stderr=subprocess.STDOUT).communicate() except OSError: return (False, '"{0}" cannot be found'.format(self.executable)) return (True, 'using "{0}" for executable'.format(self.executable)) def _get_lint_args(self, view, code, filename): if hasattr(self, 'get_lint_args'): return self.get_lint_args(view, code, filename) or [] else: lintArgs = self.lint_args or [] settings = view.settings().get('SublimeLinter', {}).get(self.language, {}) if settings: if 'lint_args' in settings: lintArgs = settings['lint_args'] cwd = settings.get('working_directory', '').encode('utf-8') if cwd and os.path.isabs(cwd) and os.path.isdir(cwd): os.chdir(cwd) return [arg.format(filename=filename) for arg in lintArgs] def built_in_check(self, view, code, filename): return '' def executable_check(self, view, code, filename): args = [self.executable] tempfilePath = None if self.input_method == INPUT_METHOD_STDIN: args.extend(self._get_lint_args(view, code, filename)) elif self.input_method == INPUT_METHOD_TEMP_FILE: if filename: filename = os.path.basename(filename) else: filename = u'view{0}'.format(view.id()) tempfilePath = os.path.join(TEMPFILES_DIR, filename) with open(tempfilePath, 'w') as f: f.write(code) args.extend(self._get_lint_args(view, code, tempfilePath)) code = u'' elif self.input_method == INPUT_METHOD_FILE: args.extend(self._get_lint_args(view, code, filename)) code = u'' else: return u'' try: process = subprocess.Popen(args, stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, startupinfo=self.get_startupinfo()) process.stdin.write(code) result = process.communicate()[0] finally: if tempfilePath: os.remove(tempfilePath) return result.strip() def parse_errors(self, view, errors, lines, errorUnderlines, violationUnderlines, warningUnderlines, errorMessages, violationMessages, warningMessages): pass def add_message(self, lineno, lines, message, messages): # Assume lineno is one-based, ST2 wants zero-based line numbers lineno -= 1 lines.add(lineno) message = message[0].upper() + message[1:] # Remove trailing period from error message if message[-1] == '.': message = message[:-1] if lineno in messages: messages[lineno].append(message) else: messages[lineno] = [message] def underline_range(self, view, lineno, position, underlines, length=1): # Assume lineno is one-based, ST2 wants zero-based line numbers lineno -= 1 line = view.full_line(view.text_point(lineno, 0)) position += line.begin() for i in xrange(length): underlines.append(sublime.Region(position + i)) def underline_regex(self, view, lineno, regex, lines, underlines, wordmatch=None, linematch=None): # Assume lineno is one-based, ST2 wants zero-based line numbers lineno -= 1 lines.add(lineno) offset = 0 line = view.full_line(view.text_point(lineno, 0)) lineText = view.substr(line) if linematch: match = re.match(linematch, lineText) if match: lineText = match.group('match') offset = match.start('match') else: return iters = re.finditer(regex, lineText) results = [(result.start('underline'), result.end('underline')) for result in iters if not wordmatch or result.group('underline') == wordmatch] # Make the lineno one-based again for underline_range lineno += 1 for start, end in results: self.underline_range(view, lineno, start + offset, underlines, end - start) def underline_word(self, view, lineno, position, underlines): # Assume lineno is one-based, ST2 wants zero-based line numbers lineno -= 1 line = view.full_line(view.text_point(lineno, 0)) position += line.begin() word = view.word(position) underlines.append(word) def run(self, view, code, filename=None): self.filename = filename if self.executable is None: errors = self.built_in_check(view, code, filename) else: errors = self.executable_check(view, code, filename) lines = set() errorUnderlines = [] # leave this here for compatibility with original plugin errorMessages = {} violationUnderlines = [] violationMessages = {} warningUnderlines = [] warningMessages = {} self.parse_errors(view, errors, lines, errorUnderlines, violationUnderlines, warningUnderlines, errorMessages, violationMessages, warningMessages) return lines, errorUnderlines, violationUnderlines, warningUnderlines, errorMessages, violationMessages, warningMessages def get_mapped_executable(self, view, default): map = view.settings().get('sublimelinter_executable_map') if map: lang = self.language.lower() if lang in map: return map[lang].encode('utf-8') return default def get_startupinfo(self): info = None if os.name == 'nt': info = subprocess.STARTUPINFO() info.dwFlags |= subprocess.STARTF_USESHOWWINDOW info.wShowWindow = subprocess.SW_HIDE return info def execute_get_output(self, args): try: return subprocess.Popen(args, self.get_startupinfo()).communicate()[0] except: return '' def jsc_path(self): '''Return the path to JavaScriptCore. Use this method in case the path has to be dynamically calculated in the future.''' return self.JSC_PATH def find_file(self, filename, view): '''Find a file with the given name, starting in the view's directory, then ascending the file hierarchy up to root.''' path = (view.file_name() or '').encode('utf-8') # quit if the view is temporary if not path: return None dirname = os.path.dirname(path) while True: path = os.path.join(dirname, filename) if os.path.isfile(path): with open(path, 'r') as f: return f.read() # if we hit root, quit parent = os.path.dirname(dirname) if parent == dirname: return None else: dirname = parent def strip_json_comments(self, json_str): stripped_json = JSON_MULTILINE_COMMENT_RE.sub('', json_str) stripped_json = JSON_SINGLELINE_COMMENT_RE.sub('', stripped_json) return json.dumps(json.loads(stripped_json)) def get_javascript_args(self, view, linter, code): path = os.path.join(self.LIB_PATH, linter) options = self.get_javascript_options(view) if options is None: options = json.dumps(view.settings().get('%s_options' % linter) or {}) self.get_javascript_engine(view) engine = self.js_engine if (engine['name'] == 'jsc'): args = [engine['wrapper'], '--', path + os.path.sep, str(code.count('\n')), options] else: args = [engine['wrapper'], path + os.path.sep, options] return args def get_javascript_options(self, view): '''Subclasses should override this if they want to provide options for a JavaScript-based linter. If the subclass cannot provide options, it should return None (or not return anything).''' return None def get_javascript_engine(self, view): if self.js_engine is None: for engine in self.JAVASCRIPT_ENGINES: if engine == 'node': try: path = self.get_mapped_executable(view, 'node') subprocess.call([path, u'-v'], startupinfo=self.get_startupinfo()) self.js_engine = { 'name': engine, 'path': path, 'wrapper': os.path.join(self.JAVASCRIPT_ENGINE_WRAPPERS_PATH, engine + '.js'), } break except OSError: pass elif engine == 'jsc': if os.path.exists(self.jsc_path()): self.js_engine = { 'name': engine, 'path': self.jsc_path(), 'wrapper': os.path.join(self.JAVASCRIPT_ENGINE_WRAPPERS_PATH, engine + '.js'), } break if self.js_engine is not None: return (True, self.js_engine['path'], 'using {0}'.format(self.JAVASCRIPT_ENGINE_NAMES[self.js_engine['name']])) # Didn't find an engine, tell the user engine_list = ', '.join(self.JAVASCRIPT_ENGINE_NAMES.values()) return (False, '', 'One of the following JavaScript engines must be installed: ' + engine_list) ================================================ FILE: sublimelinter/modules/c.py ================================================ import re from base_linter import BaseLinter, INPUT_METHOD_TEMP_FILE CONFIG = { 'language': 'C', 'executable': 'cppcheck', 'lint_args': ['--enable=style', '--quiet', '{filename}'], 'input_method': INPUT_METHOD_TEMP_FILE } class Linter(BaseLinter): CPPCHECK_RE = re.compile(r'\[.+?:(\d+?)\](.+)') def __init__(self, config): super(Linter, self).__init__(config) def parse_errors(self, view, errors, lines, errorUnderlines, violationUnderlines, warningUnderlines, errorMessages, violationMessages, warningMessages): # Go through each line in the output of cppcheck for line in errors.splitlines(): match = self.CPPCHECK_RE.match(line) if match: # The regular expression matches the line number and # the message as its two groups. lineno, message = match.group(1), match.group(2) # Remove the colon at the beginning of the message if len(message) > 0 and message[0] == ':': message = message[1:].strip() lineno = int(lineno) self.add_message(lineno, lines, message, errorMessages) ================================================ FILE: sublimelinter/modules/c_cpplint.py ================================================ import re from base_linter import BaseLinter CONFIG = { 'language': 'c_cpplint', 'executable': 'cpplint.py', 'test_existence_args': ['--help'], 'lint_args': '-', } class Linter(BaseLinter): def parse_errors(self, view, errors, lines, errorUnderlines, violationUnderlines, warningUnderlines, errorMessages, violationMessages, warningMessages): for line in errors.splitlines(): match = re.match(r'^.+:(?P\d+):\s+(?P.+)', line) if match: error, line = match.group('error'), match.group('line') self.add_message(int(line), lines, error, errorMessages) ================================================ FILE: sublimelinter/modules/coffeescript.py ================================================ import re import os from base_linter import BaseLinter CONFIG = { 'language': 'CoffeeScript', 'executable': 'coffee.cmd' if os.name == 'nt' else 'coffee', 'lint_args': ['-s', '-l'] } class Linter(BaseLinter): def parse_errors(self, view, errors, lines, errorUnderlines, violationUnderlines, warningUnderlines, errorMessages, violationMessages, warningMessages): for line in errors.splitlines(): match = re.match(r'.*?Error: Parse error on line ' r'(?P\d+): (?P.+)', line) if not match: match = re.match(r'.*?Error: (?P.+) ' r'on line (?P\d+)', line) if not match: match = re.match(r'[^:]+:(?P\d+):\d+: ' r'error: (?P.+)', line) if match: line, error = match.group('line'), match.group('error') self.add_message(int(line), lines, error, errorMessages) ================================================ FILE: sublimelinter/modules/css.py ================================================ import json from base_linter import BaseLinter CONFIG = { 'language': 'CSS' } class Linter(BaseLinter): def __init__(self, config): super(Linter, self).__init__(config) def get_executable(self, view): return self.get_javascript_engine(view) def get_lint_args(self, view, code, filename): return self.get_javascript_args(view, 'csslint', code) def parse_errors(self, view, errors, lines, errorUnderlines, violationUnderlines, warningUnderlines, errorMessages, violationMessages, warningMessages): try: errors = json.loads(errors.strip() or '[]') except ValueError: raise ValueError("Error from csslint: {0}".format(errors)) for error in errors: lineno = error['line'] if error['type'] == 'warning': messages = warningMessages underlines = warningUnderlines else: messages = errorMessages underlines = errorUnderlines self.add_message(lineno, lines, error['reason'], messages) self.underline_range(view, lineno, error['character'] - 1, underlines) ================================================ FILE: sublimelinter/modules/git_commit_message.py ================================================ from base_linter import BaseLinter CONFIG = { 'language': 'Git Commit Message' } class ErrorType: WARNING = 'warning' VIOLATION = 'violation' ERROR = 'error' class Linter(BaseLinter): def built_in_check(self, view, code, filename): lines = code.splitlines() lineno = 0 real_lineno = 0 first_line_of_message = None first_line_of_body = None errors = [] for line in lines: real_lineno += 1 if line.startswith('#'): continue if line.startswith('diff --git'): break lineno += 1 if first_line_of_message is None: if line.strip(): first_line_of_message = lineno if len(line) > 68: errors.append({ 'type': ErrorType.ERROR, 'message': 'Subject line must be 68 characters or less (github will truncate).', 'lineno': real_lineno, 'col': 68, }) elif len(line) > 50: errors.append({ 'type': ErrorType.WARNING, 'message': 'Subject line should be 50 characters or less.', 'lineno': real_lineno, 'col': 50, }) elif lineno != 1: errors.append({ 'type': ErrorType.ERROR, 'message': 'Subject must be on first line.', 'lineno': real_lineno, }) elif line[0].upper() != line[0]: errors.append({ 'type': ErrorType.VIOLATION, 'message': 'Subject line should be capitalized.', 'lineno': real_lineno, }) elif first_line_of_body is None: if len(line): first_line_of_body = lineno if lineno == first_line_of_message + 1: if len(line): errors.append({ 'message': 'Leave a blank line between the message subject and body.', 'lineno': first_line_of_message + 1, }) elif lineno > first_line_of_message + 2: errors.append({ 'message': 'Leave exactly 1 blank line between the message subject and body.', 'lineno': real_lineno, }) if first_line_of_body is not None: if len(line) > 72: errors.append({ 'message': 'Lines must not exceed 72 characters.', 'lineno': real_lineno, 'col': 72, }) return errors def parse_errors(self, view, errors, lines, errorUnderlines, violationUnderlines, warningUnderlines, errorMessages, violationMessages, warningMessages): for error in errors: error_type = error.get('type', ErrorType.ERROR) col = error.get('col', 0) messages = { ErrorType.WARNING: warningMessages, ErrorType.VIOLATION: violationMessages, ErrorType.ERROR: errorMessages, }[error_type] underlines = { ErrorType.WARNING: warningUnderlines, ErrorType.VIOLATION: violationUnderlines, ErrorType.ERROR: errorUnderlines, }[error_type] self.add_message(error['lineno'], lines, error['message'], messages) self.underline_range(view, error['lineno'], col, underlines, length=1) ================================================ FILE: sublimelinter/modules/haml.py ================================================ import re from base_linter import BaseLinter CONFIG = { 'language': 'Ruby Haml', 'executable': 'haml', 'lint_args': '-c' } class Linter(BaseLinter): def parse_errors(self, view, errors, lines, errorUnderlines, violationUnderlines, warningUnderlines, errorMessages, violationMessages, warningMessages): for line in errors.splitlines(): match = re.match(r'^.+(?P\d+):\s+(?P.+)', line) if match: error, line = match.group('error'), match.group('line') self.add_message(int(line), lines, error, errorMessages) ================================================ FILE: sublimelinter/modules/haskell.py ================================================ import re from base_linter import BaseLinter, INPUT_METHOD_FILE CONFIG = { 'language': 'haskell', 'executable': 'hlint', 'input_method': INPUT_METHOD_FILE, 'lint_args': '{filename}' } class Linter(BaseLinter): def parse_errors(self, view, errors, lines, errorUnderlines, violationUnderlines, warningUnderlines, errorMessages, violationMessages, warningMessages): i = 0 error_lines = errors.splitlines() while i < len(error_lines): error = re.match(r'^.+:(?P\d+):(?P\d+): (?P.+)', error_lines[i]) if error: message, lineno, col = error.group('error'), int(error.group('line')), int(error.group('col')) if error_lines[i + 1] == "Error message:": message = error_lines[i + 2] i += 2 lint_error = re.match(r'^(?PError|Warning): (?P.+)', message) if lint_error: error_type, message = lint_error.group('type'), lint_error.group('error') if error_type == 'Warning': messages = warningMessages underlines = warningUnderlines else: messages = errorMessages underlines = errorUnderlines self.add_message(lineno, lines, message, messages) self.underline_range(view, lineno, col - 1, underlines) i += 1 ================================================ FILE: sublimelinter/modules/html.py ================================================ # Example error messages # # line 1 column 1 - Warning: missing declaration # line 200 column 1 - Warning: discarding unexpected # line 1 column 1 - Warning: inserting missing 'title' element import re import subprocess from base_linter import BaseLinter CONFIG = { 'language': 'HTML', 'executable': 'tidy', 'lint_args': '-eq' } class Linter(BaseLinter): def get_executable(self, view): try: path = self.get_mapped_executable(view, 'tidy') version_string = subprocess.Popen([path, '-v'], startupinfo=self.get_startupinfo(), stdout=subprocess.PIPE).communicate()[0] if u'HTML5' in version_string: return (True, path, 'using tidy for executable') return (False, '', 'tidy is not ready for HTML5') except OSError: return (False, '', 'tidy cannot be found') def parse_errors(self, view, errors, lines, errorUnderlines, violationUnderlines, warningUnderlines, errorMessages, violationMessages, warningMessages): for line in errors.splitlines(): match = re.match(r'^line\s(?P\d+)\scolumn\s\d+\s-\s(?P.+)', line) if match: error, line = match.group('error'), match.group('line') self.add_message(int(line), lines, error, errorMessages) ================================================ FILE: sublimelinter/modules/java.py ================================================ import os import os.path import re from base_linter import BaseLinter, INPUT_METHOD_FILE CONFIG = { 'language': 'Java', 'executable': 'javac', 'test_existence_args': '-version', 'input_method': INPUT_METHOD_FILE } ERROR_RE = re.compile(r'^(?P.*\.java):(?P\d+): (?Pwarning: )?(?:\[\w+\] )?(?P.*)') MARK_RE = re.compile(r'^(?P\s*)\^$') class Linter(BaseLinter): def parse_errors(self, view, errors, lines, errorUnderlines, violationUnderlines, warningUnderlines, errorMessages, violationMessages, warningMessages): it = iter(errors.splitlines()) for line in it: match = re.match(ERROR_RE, line) if match: path = os.path.abspath(match.group('path')) if path != self.filename: continue lineNumber = int(match.group('line')) warning = match.group('warning') error = match.group('error') if warning: messages = warningMessages underlines = warningUnderlines else: messages = errorMessages underlines = errorUnderlines # Skip forward until we find the marker position = -1 while True: line = it.next() match = re.match(MARK_RE, line) if match: position = len(match.group('mark')) break self.add_message(lineNumber, lines, error, messages) self.underline_range(view, lineNumber, position, underlines) ================================================ FILE: sublimelinter/modules/javascript.py ================================================ import json import re import subprocess from base_linter import BaseLinter, INPUT_METHOD_TEMP_FILE CONFIG = { 'language': 'JavaScript' } class Linter(BaseLinter): GJSLINT_RE = re.compile(r'Line (?P\d+),\s*E:(?P\d+):\s*(?P.+)') def __init__(self, config): super(Linter, self).__init__(config) self.linter = None def get_executable(self, view): self.linter = view.settings().get('javascript_linter', 'jshint') if (self.linter in ('jshint', 'jslint')): return self.get_javascript_engine(view) elif (self.linter == 'gjslint'): try: path = self.get_mapped_executable(view, 'gjslint') subprocess.call([path, u'--help'], startupinfo=self.get_startupinfo()) self.input_method = INPUT_METHOD_TEMP_FILE return (True, path, 'using gjslint') except OSError: return (False, '', 'gjslint cannot be found') else: return (False, '', '"{0}" is not a valid javascript linter'.format(self.linter)) def get_lint_args(self, view, code, filename): if (self.linter == 'gjslint'): args = [] gjslint_options = view.settings().get("gjslint_options", []) args.extend(gjslint_options) args.extend([u'--nobeep', filename]) return args elif (self.linter in ('jshint', 'jslint')): return self.get_javascript_args(view, self.linter, code) else: return [] def get_javascript_options(self, view): if self.linter == 'jshint': rc_options = self.find_file('.jshintrc', view) if rc_options is not None: rc_options = self.strip_json_comments(rc_options) return json.dumps(json.loads(rc_options)) def parse_errors(self, view, errors, lines, errorUnderlines, violationUnderlines, warningUnderlines, errorMessages, violationMessages, warningMessages): if (self.linter == 'gjslint'): ignore = view.settings().get('gjslint_ignore', []) for line in errors.splitlines(): match = self.GJSLINT_RE.match(line) if match: line, errnum, message = match.group('line'), match.group('errnum'), match.group('message') if (int(errnum) not in ignore): self.add_message(int(line), lines, message, errorMessages) elif (self.linter in ('jshint', 'jslint')): try: errors = json.loads(errors.strip() or '[]') except ValueError: raise ValueError("Error from {0}: {1}".format(self.linter, errors)) for error in errors: lineno = error['line'] self.add_message(lineno, lines, error['reason'], errorMessages) self.underline_range(view, lineno, error['character'] - 1, errorUnderlines) ================================================ FILE: sublimelinter/modules/libs/capp_lint.py ================================================ #!/usr/bin/env python # # capp_lint.py - Check Objective-J source code formatting, # according to Cappuccino standards: # # http://cappuccino.org/contribute/coding-style.php # # Copyright (C) 2011 Aparajita Fishman # Permission is hereby granted, free of charge, to any person # obtaining a copy of this software and associated documentation files # (the "Software"), to deal in the Software without restriction, # including without limitation the rights to use, copy, modify, merge, # publish, distribute, sublicense, and/or sell copies of the Software, # and to permit persons to whom the Software is furnished to do so, # subject to the following conditions: # # The above copyright notice and this permission notice shall be # included in all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS # BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN # ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN # CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. from __future__ import with_statement from optparse import OptionParser from string import Template import cgi import cStringIO import os import os.path import re import sys import unittest EXIT_CODE_SHOW_HTML = 205 EXIT_CODE_SHOW_TOOLTIP = 206 def exit_show_html(html): sys.stdout.write(html.encode('utf-8')) sys.exit(EXIT_CODE_SHOW_HTML) def exit_show_tooltip(text): sys.stdout.write(text) sys.exit(EXIT_CODE_SHOW_TOOLTIP) def within_textmate(): return os.getenv('TM_APP_PATH') is not None def tabs2spaces(text, positions=None): while True: index = text.find(u'\t') if index < 0: return text spaces = u' ' * (4 - (index % 4)) text = text[0:index] + spaces + text[index + 1:] if positions is not None: positions.append(index) def relative_path(basedir, filename): if filename.find(basedir) == 0: filename = filename[len(basedir) + 1:] return filename def string_replacer(line): """Take string literals like 'hello' and replace them with empty string literals, while respecting escaping.""" r = [] in_quote = None escapes = 0 for i, c in enumerate(line): if in_quote: if not escapes and c == in_quote: in_quote = None r.append(c) continue # We're inside of a string literal. Ignore everything. else: if not escapes and (c == '"' or c == "'"): in_quote = c r.append(c) continue # Outside of a string literal, preserve everything. r.append(c) if c == '\\': escapes = (escapes + 1) % 2 else: escapes = 0 if in_quote: # Unterminated string literal. pass return "".join(r) class LintChecker(object): """Examine Objective-J code statically and generate warnings for possible errors and deviations from the coding-style standard. >>> LintChecker().lint_text('var b = 5+5;') [{'positions': [9], 'filename': '', 'lineNum': 1, 'message': 'binary operator without surrounding spaces', 'type': 2, 'line': u'var b = 5+5;'}] >>> LintChecker().lint_text(''' ... if( 1 ) { ... var b=7; ... c = 8; ... } ... ''') [{'positions': [2], 'filename': '', 'lineNum': 2, 'message': 'missing space between control statement and parentheses', 'type': 2, 'line': u'if( 1 ) {'}, {'positions': [8], 'filename': '', 'lineNum': 2, 'message': 'braces should be on their own line', 'type': 1, 'line': u'if( 1 ) {'}, {'positions': [3, 5], 'filename': '', 'lineNum': 2, 'message': 'space inside parentheses', 'type': 1, 'line': u'if( 1 ) {'}, {'positions': [7], 'filename': '', 'lineNum': 3, 'message': 'assignment operator without surrounding spaces', 'type': 2, 'line': u' var b=7;'}, {'lineNum': 4, 'message': 'accidental global variable', 'type': 1, 'line': u' c = 8;', 'filename': ''}] """ VAR_BLOCK_START_RE = re.compile(ur'''(?x) (?P\s*) # indent before a var keyword (?Pvar\s+) # var keyword and whitespace after (?P[a-zA-Z_$]\w*)\s* (?: (?P=)\s* (?P.*) | (?P[,;+\-/*%^&|=\\]) ) ''') SEPARATOR_RE = re.compile(ur'''(?x) (?P.*) # Everything up to the line separator (?P[,;+\-/*%^&|=\\]) # The line separator \s* # Optional whitespace after $ # End of expression ''') INDENTED_EXPRESSION_RE_TEMPLATE = ur'''(?x) [ ]{%d} # Placeholder for indent of first identifier that started block (?P.+) # Expression ''' VAR_BLOCK_RE_TEMPLATE = ur'''(?x) [ ]{%d} # Placeholder for indent of first identifier that started block (?P\s*) # Capture any further indent (?: (?P[\[\{].*) | (?P[a-zA-Z_$]\w*)\s* (?: (?P=)\s* (?P.*) | (?P[,;+\-/*%%^&|=\\]) ) | (?P.+) ) ''' STATEMENT_RE = re.compile(ur'''(?x) \s*((continue|do|for|function|if|else|return|switch|while|with)\b|\[+\s*[a-zA-Z_$]\w*\s+[a-zA-Z_$]\w*\s*[:\]]) ''') TRAILING_WHITESPACE_RE = re.compile(ur'^.*(\s+)$') STRIP_LINE_COMMENT_RE = re.compile(ur'(.*)\s*(?://.*|/\*.*\*/\s*)$') LINE_COMMENT_RE = re.compile(ur'\s*(?:/\*.*\*/\s*|//.*)$') COMMENT_RE = re.compile(ur'/\*.*?\*/') BLOCK_COMMENT_START_RE = re.compile(ur'\s*/\*.*(?!\*/\s*)$') BLOCK_COMMENT_END_RE = re.compile(ur'.*?\*/') METHOD_RE = ur'[-+]\s*\([a-zA-Z_$]\w*\)\s*[a-zA-Z_$]\w*' FUNCTION_RE = re.compile(ur'\s*function\s*(?P[a-zA-Z_$]\w*)?\(.*\)\s*\{?') RE_RE = re.compile(ur'(?]*\)\s*\w+|[a-zA-Z_$]\w*(\+\+|--)|([ -+*/%^&|<>!]=?|&&|\|\||<<|>>>|={1,3}|!==?)\s*[-+][\w(\[])'), 'pass': False}, # Also convert literals like 1.5e+7 to 42 so that the - or + in there is ignored for purposes of this warning. 'preprocess': STD_IGNORES + EXPONENTIAL_TO_SIMPLE, 'regex': re.compile(ur'(?<=[\w)\]"\']|([ ]))([-+*/%^]|&&?|\|\|?|<<|>>>?)(?=[\w({\["\']|(?(1)\b\b|[ ]))'), 'error': 'binary operator without surrounding spaces', 'showPositionForGroup': 2, 'type': ERROR_TYPE_WARNING }, { # Filter out possible = within @accessors 'filter': {'regex': re.compile(ur'^\s*(?:@outlet\s+)?[a-zA-Z_$]\w*\s+[a-zA-Z_$]\w*\s+@accessors\b'), 'pass': False}, 'preprocess': STD_IGNORES, 'regex': re.compile(ur'(?<=[\w)\]"\']|([ ]))(=|[-+*/%^&|]=|<<=|>>>?=)(?=[\w({\["\']|(?(1)\b\b|[ ]))'), 'error': 'assignment operator without surrounding spaces', 'showPositionForGroup': 2, 'type': ERROR_TYPE_WARNING }, { # Filter out @import statements and @implementation/method declarations 'filter': {'regex': re.compile(ur'^(@import\b|@implementation\b|\s*' + METHOD_RE + ')'), 'pass': False}, 'preprocess': STD_IGNORES, 'regex': re.compile(ur'(?<=[\w)\]"\']|([ ]))(===?|!==?|[<>]=?)(?=[\w({\["\']|(?(1)\b\b|[ ]))'), 'error': 'comparison operator without surrounding spaces', 'showPositionForGroup': 2, 'type': ERROR_TYPE_WARNING }, { 'regex': re.compile(ur'^(\s+)' + METHOD_RE + '|^\s*[-+](\()[a-zA-Z_$][\w]*\)\s*[a-zA-Z_$]\w*|^\s*[-+]\s*\([a-zA-Z_$][\w]*\)(\s+)[a-zA-Z_$]\w*'), 'error': 'extra or missing space in a method declaration', 'showPositionForGroup': 0, 'type': ERROR_TYPE_WARNING }, { # Check for brace following a class or method declaration 'regex': re.compile(ur'^(?:\s*[-+]\s*\([a-zA-Z_$]\w*\)|@implementation)\s*[a-zA-Z_$][\w]*.*?\s*(\{)\s*(?:$|//.*$)'), 'error': 'braces should be on their own line', 'showPositionForGroup': 0, 'type': ERROR_TYPE_ILLEGAL }, { 'regex': re.compile(ur'^\s*var\s+[a-zA-Z_$]\w*\s*=\s*function\s+([a-zA-Z_$]\w*)\s*\('), 'error': 'function name is ignored', 'showPositionForGroup': 1, 'skip': True, 'type': ERROR_TYPE_WARNING }, ) VAR_DECLARATIONS = ['none', 'single', 'strict'] VAR_DECLARATIONS_NONE = 0 VAR_DECLARATIONS_SINGLE = 1 VAR_DECLARATIONS_STRICT = 2 DIRS_TO_SKIP = ('.git', 'Frameworks', 'Build', 'Resources', 'CommonJS', 'Objective-J') ERROR_FORMATS = ('text', 'html') TEXT_ERROR_SINGLE_FILE_TEMPLATE = Template(u'$lineNum: $message.\n+$line\n') TEXT_ERROR_MULTI_FILE_TEMPLATE = Template(u'$filename:$lineNum: $message.\n+$line\n') def __init__(self, view=None, basedir='', var_declarations=VAR_DECLARATIONS_SINGLE, verbose=False): self.view = view self.basedir = unicode(basedir, 'utf-8') self.errors = [] self.errorFiles = [] self.filesToCheck = [] self.varDeclarations = var_declarations self.verbose = verbose self.sourcefile = None self.filename = u'' self.line = u'' self.lineNum = 0 self.varIndent = u'' self.identifierIndent = u'' self.fileChecklist = ( {'title': 'Check variable blocks', 'action': self.check_var_blocks}, ) def run_line_checks(self): for check in self.LINE_CHECKLIST: option = check.get('option') if option: default = check.get('optionDefault', False) if self.view and not self.view.settings().get(option, default): continue line = self.line originalLine = line lineFilter = check.get('filter') if lineFilter: match = lineFilter['regex'].search(line) if (match and not lineFilter['pass']) or (not match and lineFilter['pass']): continue preprocess = check.get('preprocess') if preprocess: if not isinstance(preprocess, (list, tuple)): preprocess = (preprocess,) for processor in preprocess: regex = processor.get('regex') if regex: line = regex.sub(processor.get('replace', ''), line) fnct = processor.get('function') if fnct: line = fnct(line) regex = check.get('regex') if not regex: continue match = regex.search(line) if not match: continue positions = [] groups = check.get('showPositionForGroup') if (check.get('id') == 'tabs'): line = tabs2spaces(line, positions=positions) elif groups is not None: line = tabs2spaces(line) if not isinstance(groups, (list, tuple)): groups = (groups,) for match in regex.finditer(line): for group in groups: if group > 0: start = match.start(group) if start >= 0: positions.append(start) else: # group 0 means show the first non-empty match for i in range(1, len(match.groups()) + 1): if match.start(i) >= 0: positions.append(match.start(i)) break if positions: self.error(check['error'], line=originalLine, positions=positions, type=check['type']) def next_statement(self, expect_line=False, check_line=True): try: while True: raw_line = self.sourcefile.next() # strip EOL if raw_line[-1] == '\n': # ... unless this is the last line which might not have a \n. raw_line = raw_line[:-1] try: self.line = unicode(raw_line, 'utf-8', 'strict') # convert to Unicode self.lineNum += 1 except UnicodeDecodeError: self.line = unicode(raw_line, 'utf-8', 'replace') self.lineNum += 1 self.error('line contains invalid unicode character(s)', type=self.ERROR_TYPE_ILLEGAL) if self.verbose: print u'%d: %s' % (self.lineNum, tabs2spaces(self.line)) if check_line: self.run_line_checks() if not self.is_statement(): continue return True except StopIteration: if expect_line: self.error('unexpected EOF', type=self.ERROR_TYPE_ILLEGAL) raise def is_statement(self): # Skip empty lines if len(self.line.strip()) == 0: return False # See if we have a line comment, skip that match = self.LINE_COMMENT_RE.match(self.line) if match: return False # Match a block comment start next so we can find its end, # otherwise we might get false matches on the contents of the block comment. match = self.BLOCK_COMMENT_START_RE.match(self.line) if match: self.block_comment() return False return True def is_expression(self): match = self.STATEMENT_RE.match(self.line) return match is None def strip_comment(self): match = self.STRIP_LINE_COMMENT_RE.match(self.expression) if match: self.expression = match.group(1) def get_expression(self, lineMatch): groupdict = lineMatch.groupdict() self.expression = groupdict.get('expression') if self.expression is None: self.expression = groupdict.get('bracket') if self.expression is None: self.expression = groupdict.get('indented_expression') if self.expression is None: self.expression = '' return # Remove all quoted strings from the expression so that we don't # count unmatched pairs inside the strings. self.expression = string_replacer(self.expression) self.strip_comment() self.expression = self.expression.strip() def block_comment(self): 'Find the end of a block comment' commentOpenCount = self.line.count('/*') commentOpenCount -= self.line.count('*/') # If there is an open comment block, eat it if commentOpenCount: if self.verbose: print u'%d: BLOCK COMMENT START' % self.lineNum else: return match = None while not match and self.next_statement(expect_line=True, check_line=False): match = self.BLOCK_COMMENT_END_RE.match(self.line) if self.verbose: print u'%d: BLOCK COMMENT END' % self.lineNum def balance_pairs(self, squareOpenCount, curlyOpenCount, parenOpenCount): # The following lines have to be indented at least as much as the first identifier # after the var keyword at the start of the block. if self.verbose: print "%d: BALANCE BRACKETS: '['=%d, '{'=%d, '('=%d" % (self.lineNum, squareOpenCount, curlyOpenCount, parenOpenCount) lineRE = re.compile(self.INDENTED_EXPRESSION_RE_TEMPLATE % len(self.identifierIndent)) while True: # If the expression has open brackets and is terminated, it's an error match = self.SEPARATOR_RE.match(self.expression) if match and match.group('separator') == ';': unterminated = [] if squareOpenCount: unterminated.append('[') if curlyOpenCount: unterminated.append('{') if parenOpenCount: unterminated.append('(') self.error('unbalanced %s' % ' and '.join(unterminated), type=self.ERROR_TYPE_ILLEGAL) return False self.next_statement(expect_line=True) match = lineRE.match(self.line) if not match: # If it doesn't match, the indent is wrong check the whole line self.error('incorrect indentation') self.expression = self.line self.strip_comment() else: # It matches, extract the expression self.get_expression(match) # Update the bracket counts squareOpenCount += self.expression.count('[') squareOpenCount -= self.expression.count(']') curlyOpenCount += self.expression.count('{') curlyOpenCount -= self.expression.count('}') parenOpenCount += self.expression.count('(') parenOpenCount -= self.expression.count(')') if squareOpenCount == 0 and curlyOpenCount == 0 and parenOpenCount == 0: if self.verbose: print u'%d: BRACKETS BALANCED' % self.lineNum # The brackets are closed, this line must be separated match = self.SEPARATOR_RE.match(self.expression) if not match: self.error('missing statement separator', type=self.ERROR_TYPE_ILLEGAL) return False return True def pairs_balanced(self, lineMatchOrBlockMatch): groups = lineMatchOrBlockMatch.groupdict() if 'assignment' in groups or 'bracket' in groups: squareOpenCount = self.expression.count('[') squareOpenCount -= self.expression.count(']') curlyOpenCount = self.expression.count('{') curlyOpenCount -= self.expression.count('}') parenOpenCount = self.expression.count('(') parenOpenCount -= self.expression.count(')') if squareOpenCount or curlyOpenCount or parenOpenCount: # If the brackets were not properly closed or the statement was # missing a separator, skip the rest of the var block. if not self.balance_pairs(squareOpenCount, curlyOpenCount, parenOpenCount): return False return True def var_block(self, blockMatch): """ Parse a var block, return a tuple (haveLine, isSingleVar), where haveLine indicates whether self.line is the next line to be parsed. """ # Keep track of whether this var block has multiple declarations isSingleVar = True # Keep track of the indent of the var keyword to compare with following lines self.varIndent = blockMatch.group('indent') # Keep track of how far the first variable name is indented to make sure # following lines line up with that self.identifierIndent = self.varIndent + blockMatch.group('var') # Check the expression to see if we have any open [ or { or /* self.get_expression(blockMatch) if not self.pairs_balanced(blockMatch): return (False, False) separator = '' if self.expression: match = self.SEPARATOR_RE.match(self.expression) if not match: self.error('missing statement separator', type=self.ERROR_TYPE_ILLEGAL) else: separator = match.group('separator') elif blockMatch.group('separator'): separator = blockMatch.group('separator') # If the block has a semicolon, there should be no more lines in the block blockHasSemicolon = separator == ';' # We may not catch an error till after the line that is wrong, so keep # the most recent declaration and its line number. lastBlockLine = self.line lastBlockLineNum = self.lineNum # Now construct an RE that will match any lines indented at least as much # as the var keyword that started the block. blockRE = re.compile(self.VAR_BLOCK_RE_TEMPLATE % len(self.identifierIndent)) while self.next_statement(expect_line=not blockHasSemicolon): if not self.is_statement(): continue # Is the line indented at least as much as the var keyword that started the block? match = blockRE.match(self.line) if match: if self.is_expression(): lastBlockLine = self.line lastBlockLineNum = self.lineNum # If the line is indented farther than the first identifier in the block, # it is considered a formatting error. if match.group('indent') and not match.group('indented_expression'): self.error('incorrect indentation') self.get_expression(match) if not self.pairs_balanced(match): return (False, isSingleVar) if self.expression: separatorMatch = self.SEPARATOR_RE.match(self.expression) if separatorMatch is None: # If the assignment does not have a separator, it's an error self.error('missing statement separator', type=self.ERROR_TYPE_ILLEGAL) else: separator = separatorMatch.group('separator') if blockHasSemicolon: # If the block already has a semicolon, we have an accidental global declaration self.error('accidental global variable', type=self.ERROR_TYPE_ILLEGAL) elif (separator == ';'): blockHasSemicolon = True elif match.group('separator'): separator = match.group('separator') isSingleVar = False else: # If the line is a control statement of some kind, then it should not be indented this far. self.error('statement should be outdented from preceding var block') return (True, False) else: # If the line does not match, it is not an assignment or is outdented from the block. # In either case, the block is considered closed. If the most recent separator was not ';', # the block was not properly terminated. if separator != ';': self.error('unterminated var block', lineNum=lastBlockLineNum, line=lastBlockLine, type=self.ERROR_TYPE_ILLEGAL) return (True, isSingleVar) def check_var_blocks(self): lastStatementWasVar = False lastVarWasSingle = False haveLine = True while True: if not haveLine: haveLine = self.next_statement() if not self.is_statement(): haveLine = False continue match = self.VAR_BLOCK_START_RE.match(self.line) if match is None: lastStatementWasVar = False haveLine = False continue # It might be a function definition, in which case we continue expression = match.group('expression') if expression: functionMatch = self.FUNCTION_RE.match(expression) if functionMatch: lastStatementWasVar = False haveLine = False continue # Now we have the start of a variable block if self.verbose: print u'%d: VAR BLOCK' % self.lineNum varLineNum = self.lineNum varLine = self.line haveLine, isSingleVar = self.var_block(match) if self.verbose: print u'%d: END VAR BLOCK:' % self.lineNum, if isSingleVar: print u'SINGLE' else: print u'MULTIPLE' if lastStatementWasVar and self.varDeclarations != self.VAR_DECLARATIONS_NONE: if (self.varDeclarations == self.VAR_DECLARATIONS_SINGLE and lastVarWasSingle and isSingleVar) or \ (self.varDeclarations == self.VAR_DECLARATIONS_STRICT and (lastVarWasSingle or isSingleVar)): self.error('consecutive var declarations', lineNum=varLineNum, line=varLine) lastStatementWasVar = True lastVarWasSingle = isSingleVar def run_file_checks(self): for check in self.fileChecklist: self.sourcefile.seek(0) self.lineNum = 0 if self.verbose: print u'%s: %s' % (check['title'], self.sourcefile.name) check['action']() def lint(self, filesToCheck): # Recursively walk any directories and eliminate duplicates self.filesToCheck = [] for filename in filesToCheck: filename = unicode(filename, 'utf-8') fullpath = os.path.join(self.basedir, filename) if fullpath not in self.filesToCheck: if os.path.isdir(fullpath): for root, dirs, files in os.walk(fullpath): for skipDir in self.DIRS_TO_SKIP: if skipDir in dirs: dirs.remove(skipDir) for filename in files: if not filename.endswith('.j'): continue fullpath = os.path.join(root, filename) if fullpath not in self.filesToCheck: self.filesToCheck.append(fullpath) else: self.filesToCheck.append(fullpath) for filename in self.filesToCheck: try: with open(filename) as self.sourcefile: self.filename = relative_path(self.basedir, filename) self.run_file_checks() except IOError: self.lineNum = 0 self.line = None self.error('file not found', type=self.ERROR_TYPE_ILLEGAL) except StopIteration: if self.verbose: print u'EOF\n' pass def lint_text(self, text, filename=""): self.filename = filename self.filesToCheck = [] try: self.sourcefile = cStringIO.StringIO(text) self.run_file_checks() except StopIteration: if self.verbose: print u'EOF\n' pass return self.errors def count_files_checked(self): return len(self.filesToCheck) def error(self, message, **kwargs): info = { 'filename': self.filename, 'message': message, 'type': kwargs.get('type', self.ERROR_TYPE_WARNING) } line = kwargs.get('line', self.line) lineNum = kwargs.get('lineNum', self.lineNum) if line and lineNum: info['line'] = tabs2spaces(line) info['lineNum'] = lineNum positions = kwargs.get('positions') if positions: info['positions'] = positions self.errors.append(info) if self.filename not in self.errorFiles: self.errorFiles.append(self.filename) def has_errors(self): return len(self.errors) != 0 def print_errors(self, format='text'): if not self.errors: return if format == 'text': self.print_text_errors() elif format == 'html': self.print_textmate_html_errors() elif format == 'tooltip': self.print_tooltip_errors() def print_text_errors(self): sys.stdout.write('%d error' % len(self.errors)) if len(self.errors) > 1: sys.stdout.write('s') if len(self.filesToCheck) == 1: template = self.TEXT_ERROR_SINGLE_FILE_TEMPLATE else: sys.stdout.write(' in %d files' % len(self.errorFiles)) template = self.TEXT_ERROR_MULTI_FILE_TEMPLATE sys.stdout.write(':\n\n') for error in self.errors: if 'lineNum' in error and 'line' in error: sys.stdout.write(template.substitute(error).encode('utf-8')) if error.get('positions'): markers = ' ' * len(error['line']) for position in error['positions']: markers = markers[:position] + '^' + markers[position + 1:] # Add a space at the beginning of the markers to account for the '+' at the beginning # of the source line. sys.stdout.write(' %s\n' % markers) else: sys.stdout.write('%s: %s.\n' % (error['filename'], error['message'])) sys.stdout.write('\n') def print_textmate_html_errors(self): html = """ Cappuccino Lint Report """ html += '

Results: %d error' % len(self.errors) if len(self.errors) > 1: html += 's' if len(self.filesToCheck) > 1: html += ' in %d files' % len(self.errorFiles) html += '

' for error in self.errors: message = cgi.escape(error['message']) if len(self.filesToCheck) > 1: filename = cgi.escape(error['filename']) + ':' else: filename = '' html += '

' if 'line' in error and 'lineNum' in error: filepath = cgi.escape(os.path.join(self.basedir, error['filename'])) lineNum = error['lineNum'] line = error['line'] positions = error.get('positions') firstPos = -1 source = '' if positions: firstPos = positions[0] + 1 lastPos = 0 for pos in error.get('positions'): if pos < len(line): charToHighlight = line[pos] else: charToHighlight = '' source += '%s%s' % (cgi.escape(line[lastPos:pos]), cgi.escape(charToHighlight)) lastPos = pos + 1 if lastPos <= len(line): source += cgi.escape(line[lastPos:]) else: source = line link = '' % (filepath, lineNum, firstPos) if len(self.filesToCheck) > 1: errorMsg = '%s%d: %s' % (filename, lineNum, message) else: errorMsg = '%d: %s' % (lineNum, message) html += '%(link)s%(errorMsg)s

\n

%(link)s%(source)s

\n' % {'link': link, 'errorMsg': errorMsg, 'source': source} else: html += '%s%s

\n' % (filename, message) html += """ """ exit_show_html(html) class MiscTest(unittest.TestCase): def test_string_replacer(self): self.assertEquals(string_replacer("x = 'hello';"), "x = '';") self.assertEquals(string_replacer("x = '\\' hello';"), "x = '';") self.assertEquals(string_replacer("x = '\\\\';"), "x = '';") self.assertEquals(string_replacer("""x = '"string in string"';"""), "x = '';") self.assertEquals(string_replacer('x = "hello";'), 'x = "";') self.assertEquals(string_replacer('x = "\\" hello";'), 'x = "";') self.assertEquals(string_replacer('x = "\\\\";'), 'x = "";') self.assertEquals(string_replacer('''x = "'";'''), 'x = "";') class LintCheckerTest(unittest.TestCase): def test_exponential_notation(self): """Test that exponential notation such as 1.1e-6 doesn't cause a warning about missing whitespace.""" # This should not report "binary operator without surrounding spaces". self.assertEquals(LintChecker().lint_text("a = 2.1e-6;"), []) self.assertEquals(LintChecker().lint_text("a = 2.1e+6;"), []) self.assertEquals(LintChecker().lint_text("a = 2e-0;"), []) self.assertEquals(LintChecker().lint_text("a = 2e+0;"), []) # But this should. self.assertEquals(LintChecker().lint_text("a = 1.1e-6+2e2;"), [{'positions': [6], 'filename': '', 'lineNum': 1, 'message': 'binary operator without surrounding spaces', 'type': 2, 'line': u'a = 1.1e-6+2e2;'}]) def test_function_types(self): """Test that function definitions like function(/*CPString*/key) don't cause warnings about surrounding spaces.""" # This should not report "binary operator without surrounding spaces". self.assertEquals(LintChecker().lint_text("var resolveMultipleValues = function(/*CPString*/key, /*CPDictionary*/bindings, /*GSBindingOperationKind*/operation)"), []) def test_unary_plus(self): """Test that = +, like in `x = +y;`, doesn't cause a warning.""" # + converts number in a string to a number. self.assertEquals(LintChecker().lint_text("var y = +x;"), []) def test_string_escaping(self): """Test that string literals are not parsed as syntax, even when they end with a double backslash.""" self.assertEquals(LintChecker().lint_text('var x = "(\\\\";'), []) if __name__ == '__main__': usage = 'usage: %prog [options] [file ... | -]' parser = OptionParser(usage=usage, version='1.02') parser.add_option('-f', '--format', action='store', type='string', dest='format', default='text', help='the format to use for the report: text (default) or html (HTML in which errors can be clicked on to view in TextMate)') parser.add_option('-b', '--basedir', action='store', type='string', dest='basedir', help='the base directory relative to which filenames are resolved, defaults to the current working directory') parser.add_option('-d', '--var-declarations', action='store', type='string', dest='var_declarations', default='single', help='set the policy for flagging consecutive var declarations (%s)' % ', '.join(LintChecker.VAR_DECLARATIONS)) parser.add_option('-v', '--verbose', action='store_true', dest='verbose', default=False, help='show what lint is doing') parser.add_option('-q', '--quiet', action='store_true', dest='quiet', default=False, help='do not display errors, only return an exit code') (options, args) = parser.parse_args() if options.var_declarations not in LintChecker.VAR_DECLARATIONS: parser.error('--var-declarations must be one of [' + ', '.join(LintChecker.VAR_DECLARATIONS) + ']') if options.verbose and options.quiet: parser.error('options -v/--verbose and -q/--quiet are mutually exclusive') options.format = options.format.lower() if not options.format in LintChecker.ERROR_FORMATS: parser.error('format must be one of ' + '/'.join(LintChecker.ERROR_FORMATS)) if options.format == 'html' and not within_textmate(): parser.error('html format can only be used within TextMate.') if options.basedir: basedir = options.basedir if basedir[-1] == '/': basedir = basedir[:-1] else: basedir = os.getcwd() # We accept a list of filenames (relative to the cwd) either from the command line or from stdin filenames = args if args and args[0] == '-': filenames = [name.rstrip() for name in sys.stdin.readlines()] if not filenames: print usage.replace('%prog', os.path.basename(sys.argv[0])) sys.exit(0) checker = LintChecker(basedir=basedir, view=None, var_declarations=LintChecker.VAR_DECLARATIONS.index(options.var_declarations), verbose=options.verbose) pathsToCheck = [] for filename in filenames: filename = filename.strip('"\'') path = os.path.join(basedir, filename) if (os.path.isdir(path) and not path.endswith('Frameworks')) or filename.endswith('.j'): pathsToCheck.append(relative_path(basedir, filename)) if len(pathsToCheck) == 0: if within_textmate(): exit_show_tooltip('No Objective-J files found.') sys.exit(0) checker.lint(pathsToCheck) if checker.has_errors(): if not options.quiet: checker.print_errors(options.format) sys.exit(1) else: if within_textmate(): exit_show_tooltip('Everything looks clean.') sys.exit(0) ================================================ FILE: sublimelinter/modules/libs/csslint/csslint-node.js ================================================ /*! CSSLint Copyright (c) 2011 Nicole Sullivan and Nicholas C. Zakas. All rights reserved. Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ /* Build time: 17-January-2013 10:55:01 */ /*! Parser-Lib Copyright (c) 2009-2011 Nicholas C. Zakas. All rights reserved. Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ /* Version v0.2.2, Build time: 17-January-2013 10:26:34 */ var parserlib = {}; (function(){ /** * A generic base to inherit from for any object * that needs event handling. * @class EventTarget * @constructor */ function EventTarget(){ /** * The array of listeners for various events. * @type Object * @property _listeners * @private */ this._listeners = {}; } EventTarget.prototype = { //restore constructor constructor: EventTarget, /** * Adds a listener for a given event type. * @param {String} type The type of event to add a listener for. * @param {Function} listener The function to call when the event occurs. * @return {void} * @method addListener */ addListener: function(type, listener){ if (!this._listeners[type]){ this._listeners[type] = []; } this._listeners[type].push(listener); }, /** * Fires an event based on the passed-in object. * @param {Object|String} event An object with at least a 'type' attribute * or a string indicating the event name. * @return {void} * @method fire */ fire: function(event){ if (typeof event == "string"){ event = { type: event }; } if (typeof event.target != "undefined"){ event.target = this; } if (typeof event.type == "undefined"){ throw new Error("Event object missing 'type' property."); } if (this._listeners[event.type]){ //create a copy of the array and use that so listeners can't chane var listeners = this._listeners[event.type].concat(); for (var i=0, len=listeners.length; i < len; i++){ listeners[i].call(this, event); } } }, /** * Removes a listener for a given event type. * @param {String} type The type of event to remove a listener from. * @param {Function} listener The function to remove from the event. * @return {void} * @method removeListener */ removeListener: function(type, listener){ if (this._listeners[type]){ var listeners = this._listeners[type]; for (var i=0, len=listeners.length; i < len; i++){ if (listeners[i] === listener){ listeners.splice(i, 1); break; } } } } }; /** * Convenient way to read through strings. * @namespace parserlib.util * @class StringReader * @constructor * @param {String} text The text to read. */ function StringReader(text){ /** * The input text with line endings normalized. * @property _input * @type String * @private */ this._input = text.replace(/\n\r?/g, "\n"); /** * The row for the character to be read next. * @property _line * @type int * @private */ this._line = 1; /** * The column for the character to be read next. * @property _col * @type int * @private */ this._col = 1; /** * The index of the character in the input to be read next. * @property _cursor * @type int * @private */ this._cursor = 0; } StringReader.prototype = { //restore constructor constructor: StringReader, //------------------------------------------------------------------------- // Position info //------------------------------------------------------------------------- /** * Returns the column of the character to be read next. * @return {int} The column of the character to be read next. * @method getCol */ getCol: function(){ return this._col; }, /** * Returns the row of the character to be read next. * @return {int} The row of the character to be read next. * @method getLine */ getLine: function(){ return this._line ; }, /** * Determines if you're at the end of the input. * @return {Boolean} True if there's no more input, false otherwise. * @method eof */ eof: function(){ return (this._cursor == this._input.length); }, //------------------------------------------------------------------------- // Basic reading //------------------------------------------------------------------------- /** * Reads the next character without advancing the cursor. * @param {int} count How many characters to look ahead (default is 1). * @return {String} The next character or null if there is no next character. * @method peek */ peek: function(count){ var c = null; count = (typeof count == "undefined" ? 1 : count); //if we're not at the end of the input... if (this._cursor < this._input.length){ //get character and increment cursor and column c = this._input.charAt(this._cursor + count - 1); } return c; }, /** * Reads the next character from the input and adjusts the row and column * accordingly. * @return {String} The next character or null if there is no next character. * @method read */ read: function(){ var c = null; //if we're not at the end of the input... if (this._cursor < this._input.length){ //if the last character was a newline, increment row count //and reset column count if (this._input.charAt(this._cursor) == "\n"){ this._line++; this._col=1; } else { this._col++; } //get character and increment cursor and column c = this._input.charAt(this._cursor++); } return c; }, //------------------------------------------------------------------------- // Misc //------------------------------------------------------------------------- /** * Saves the current location so it can be returned to later. * @method mark * @return {void} */ mark: function(){ this._bookmark = { cursor: this._cursor, line: this._line, col: this._col }; }, reset: function(){ if (this._bookmark){ this._cursor = this._bookmark.cursor; this._line = this._bookmark.line; this._col = this._bookmark.col; delete this._bookmark; } }, //------------------------------------------------------------------------- // Advanced reading //------------------------------------------------------------------------- /** * Reads up to and including the given string. Throws an error if that * string is not found. * @param {String} pattern The string to read. * @return {String} The string when it is found. * @throws Error when the string pattern is not found. * @method readTo */ readTo: function(pattern){ var buffer = "", c; /* * First, buffer must be the same length as the pattern. * Then, buffer must end with the pattern or else reach the * end of the input. */ while (buffer.length < pattern.length || buffer.lastIndexOf(pattern) != buffer.length - pattern.length){ c = this.read(); if (c){ buffer += c; } else { throw new Error("Expected \"" + pattern + "\" at line " + this._line + ", col " + this._col + "."); } } return buffer; }, /** * Reads characters while each character causes the given * filter function to return true. The function is passed * in each character and either returns true to continue * reading or false to stop. * @param {Function} filter The function to read on each character. * @return {String} The string made up of all characters that passed the * filter check. * @method readWhile */ readWhile: function(filter){ var buffer = "", c = this.read(); while(c !== null && filter(c)){ buffer += c; c = this.read(); } return buffer; }, /** * Reads characters that match either text or a regular expression and * returns those characters. If a match is found, the row and column * are adjusted; if no match is found, the reader's state is unchanged. * reading or false to stop. * @param {String|RegExp} matchter If a string, then the literal string * value is searched for. If a regular expression, then any string * matching the pattern is search for. * @return {String} The string made up of all characters that matched or * null if there was no match. * @method readMatch */ readMatch: function(matcher){ var source = this._input.substring(this._cursor), value = null; //if it's a string, just do a straight match if (typeof matcher == "string"){ if (source.indexOf(matcher) === 0){ value = this.readCount(matcher.length); } } else if (matcher instanceof RegExp){ if (matcher.test(source)){ value = this.readCount(RegExp.lastMatch.length); } } return value; }, /** * Reads a given number of characters. If the end of the input is reached, * it reads only the remaining characters and does not throw an error. * @param {int} count The number of characters to read. * @return {String} The string made up the read characters. * @method readCount */ readCount: function(count){ var buffer = ""; while(count--){ buffer += this.read(); } return buffer; } }; /** * Type to use when a syntax error occurs. * @class SyntaxError * @namespace parserlib.util * @constructor * @param {String} message The error message. * @param {int} line The line at which the error occurred. * @param {int} col The column at which the error occurred. */ function SyntaxError(message, line, col){ /** * The column at which the error occurred. * @type int * @property col */ this.col = col; /** * The line at which the error occurred. * @type int * @property line */ this.line = line; /** * The text representation of the unit. * @type String * @property text */ this.message = message; } //inherit from Error SyntaxError.prototype = new Error(); /** * Base type to represent a single syntactic unit. * @class SyntaxUnit * @namespace parserlib.util * @constructor * @param {String} text The text of the unit. * @param {int} line The line of text on which the unit resides. * @param {int} col The column of text on which the unit resides. */ function SyntaxUnit(text, line, col, type){ /** * The column of text on which the unit resides. * @type int * @property col */ this.col = col; /** * The line of text on which the unit resides. * @type int * @property line */ this.line = line; /** * The text representation of the unit. * @type String * @property text */ this.text = text; /** * The type of syntax unit. * @type int * @property type */ this.type = type; } /** * Create a new syntax unit based solely on the given token. * Convenience method for creating a new syntax unit when * it represents a single token instead of multiple. * @param {Object} token The token object to represent. * @return {parserlib.util.SyntaxUnit} The object representing the token. * @static * @method fromToken */ SyntaxUnit.fromToken = function(token){ return new SyntaxUnit(token.value, token.startLine, token.startCol); }; SyntaxUnit.prototype = { //restore constructor constructor: SyntaxUnit, /** * Returns the text representation of the unit. * @return {String} The text representation of the unit. * @method valueOf */ valueOf: function(){ return this.toString(); }, /** * Returns the text representation of the unit. * @return {String} The text representation of the unit. * @method toString */ toString: function(){ return this.text; } }; /*global StringReader, SyntaxError*/ /** * Generic TokenStream providing base functionality. * @class TokenStreamBase * @namespace parserlib.util * @constructor * @param {String|StringReader} input The text to tokenize or a reader from * which to read the input. */ function TokenStreamBase(input, tokenData){ /** * The string reader for easy access to the text. * @type StringReader * @property _reader * @private */ this._reader = input ? new StringReader(input.toString()) : null; /** * Token object for the last consumed token. * @type Token * @property _token * @private */ this._token = null; /** * The array of token information. * @type Array * @property _tokenData * @private */ this._tokenData = tokenData; /** * Lookahead token buffer. * @type Array * @property _lt * @private */ this._lt = []; /** * Lookahead token buffer index. * @type int * @property _ltIndex * @private */ this._ltIndex = 0; this._ltIndexCache = []; } /** * Accepts an array of token information and outputs * an array of token data containing key-value mappings * and matching functions that the TokenStream needs. * @param {Array} tokens An array of token descriptors. * @return {Array} An array of processed token data. * @method createTokenData * @static */ TokenStreamBase.createTokenData = function(tokens){ var nameMap = [], typeMap = {}, tokenData = tokens.concat([]), i = 0, len = tokenData.length+1; tokenData.UNKNOWN = -1; tokenData.unshift({name:"EOF"}); for (; i < len; i++){ nameMap.push(tokenData[i].name); tokenData[tokenData[i].name] = i; if (tokenData[i].text){ typeMap[tokenData[i].text] = i; } } tokenData.name = function(tt){ return nameMap[tt]; }; tokenData.type = function(c){ return typeMap[c]; }; return tokenData; }; TokenStreamBase.prototype = { //restore constructor constructor: TokenStreamBase, //------------------------------------------------------------------------- // Matching methods //------------------------------------------------------------------------- /** * Determines if the next token matches the given token type. * If so, that token is consumed; if not, the token is placed * back onto the token stream. You can pass in any number of * token types and this will return true if any of the token * types is found. * @param {int|int[]} tokenTypes Either a single token type or an array of * token types that the next token might be. If an array is passed, * it's assumed that the token can be any of these. * @param {variant} channel (Optional) The channel to read from. If not * provided, reads from the default (unnamed) channel. * @return {Boolean} True if the token type matches, false if not. * @method match */ match: function(tokenTypes, channel){ //always convert to an array, makes things easier if (!(tokenTypes instanceof Array)){ tokenTypes = [tokenTypes]; } var tt = this.get(channel), i = 0, len = tokenTypes.length; while(i < len){ if (tt == tokenTypes[i++]){ return true; } } //no match found, put the token back this.unget(); return false; }, /** * Determines if the next token matches the given token type. * If so, that token is consumed; if not, an error is thrown. * @param {int|int[]} tokenTypes Either a single token type or an array of * token types that the next token should be. If an array is passed, * it's assumed that the token must be one of these. * @param {variant} channel (Optional) The channel to read from. If not * provided, reads from the default (unnamed) channel. * @return {void} * @method mustMatch */ mustMatch: function(tokenTypes, channel){ var token; //always convert to an array, makes things easier if (!(tokenTypes instanceof Array)){ tokenTypes = [tokenTypes]; } if (!this.match.apply(this, arguments)){ token = this.LT(1); throw new SyntaxError("Expected " + this._tokenData[tokenTypes[0]].name + " at line " + token.startLine + ", col " + token.startCol + ".", token.startLine, token.startCol); } }, //------------------------------------------------------------------------- // Consuming methods //------------------------------------------------------------------------- /** * Keeps reading from the token stream until either one of the specified * token types is found or until the end of the input is reached. * @param {int|int[]} tokenTypes Either a single token type or an array of * token types that the next token should be. If an array is passed, * it's assumed that the token must be one of these. * @param {variant} channel (Optional) The channel to read from. If not * provided, reads from the default (unnamed) channel. * @return {void} * @method advance */ advance: function(tokenTypes, channel){ while(this.LA(0) !== 0 && !this.match(tokenTypes, channel)){ this.get(); } return this.LA(0); }, /** * Consumes the next token from the token stream. * @return {int} The token type of the token that was just consumed. * @method get */ get: function(channel){ var tokenInfo = this._tokenData, reader = this._reader, value, i =0, len = tokenInfo.length, found = false, token, info; //check the lookahead buffer first if (this._lt.length && this._ltIndex >= 0 && this._ltIndex < this._lt.length){ i++; this._token = this._lt[this._ltIndex++]; info = tokenInfo[this._token.type]; //obey channels logic while((info.channel !== undefined && channel !== info.channel) && this._ltIndex < this._lt.length){ this._token = this._lt[this._ltIndex++]; info = tokenInfo[this._token.type]; i++; } //here be dragons if ((info.channel === undefined || channel === info.channel) && this._ltIndex <= this._lt.length){ this._ltIndexCache.push(i); return this._token.type; } } //call token retriever method token = this._getToken(); //if it should be hidden, don't save a token if (token.type > -1 && !tokenInfo[token.type].hide){ //apply token channel token.channel = tokenInfo[token.type].channel; //save for later this._token = token; this._lt.push(token); //save space that will be moved (must be done before array is truncated) this._ltIndexCache.push(this._lt.length - this._ltIndex + i); //keep the buffer under 5 items if (this._lt.length > 5){ this._lt.shift(); } //also keep the shift buffer under 5 items if (this._ltIndexCache.length > 5){ this._ltIndexCache.shift(); } //update lookahead index this._ltIndex = this._lt.length; } /* * Skip to the next token if: * 1. The token type is marked as hidden. * 2. The token type has a channel specified and it isn't the current channel. */ info = tokenInfo[token.type]; if (info && (info.hide || (info.channel !== undefined && channel !== info.channel))){ return this.get(channel); } else { //return just the type return token.type; } }, /** * Looks ahead a certain number of tokens and returns the token type at * that position. This will throw an error if you lookahead past the * end of input, past the size of the lookahead buffer, or back past * the first token in the lookahead buffer. * @param {int} The index of the token type to retrieve. 0 for the * current token, 1 for the next, -1 for the previous, etc. * @return {int} The token type of the token in the given position. * @method LA */ LA: function(index){ var total = index, tt; if (index > 0){ //TODO: Store 5 somewhere if (index > 5){ throw new Error("Too much lookahead."); } //get all those tokens while(total){ tt = this.get(); total--; } //unget all those tokens while(total < index){ this.unget(); total++; } } else if (index < 0){ if(this._lt[this._ltIndex+index]){ tt = this._lt[this._ltIndex+index].type; } else { throw new Error("Too much lookbehind."); } } else { tt = this._token.type; } return tt; }, /** * Looks ahead a certain number of tokens and returns the token at * that position. This will throw an error if you lookahead past the * end of input, past the size of the lookahead buffer, or back past * the first token in the lookahead buffer. * @param {int} The index of the token type to retrieve. 0 for the * current token, 1 for the next, -1 for the previous, etc. * @return {Object} The token of the token in the given position. * @method LA */ LT: function(index){ //lookahead first to prime the token buffer this.LA(index); //now find the token, subtract one because _ltIndex is already at the next index return this._lt[this._ltIndex+index-1]; }, /** * Returns the token type for the next token in the stream without * consuming it. * @return {int} The token type of the next token in the stream. * @method peek */ peek: function(){ return this.LA(1); }, /** * Returns the actual token object for the last consumed token. * @return {Token} The token object for the last consumed token. * @method token */ token: function(){ return this._token; }, /** * Returns the name of the token for the given token type. * @param {int} tokenType The type of token to get the name of. * @return {String} The name of the token or "UNKNOWN_TOKEN" for any * invalid token type. * @method tokenName */ tokenName: function(tokenType){ if (tokenType < 0 || tokenType > this._tokenData.length){ return "UNKNOWN_TOKEN"; } else { return this._tokenData[tokenType].name; } }, /** * Returns the token type value for the given token name. * @param {String} tokenName The name of the token whose value should be returned. * @return {int} The token type value for the given token name or -1 * for an unknown token. * @method tokenName */ tokenType: function(tokenName){ return this._tokenData[tokenName] || -1; }, /** * Returns the last consumed token to the token stream. * @method unget */ unget: function(){ //if (this._ltIndex > -1){ if (this._ltIndexCache.length){ this._ltIndex -= this._ltIndexCache.pop();//--; this._token = this._lt[this._ltIndex - 1]; } else { throw new Error("Too much lookahead."); } } }; parserlib.util = { StringReader: StringReader, SyntaxError : SyntaxError, SyntaxUnit : SyntaxUnit, EventTarget : EventTarget, TokenStreamBase : TokenStreamBase }; })(); /* Parser-Lib Copyright (c) 2009-2011 Nicholas C. Zakas. All rights reserved. Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ /* Version v0.2.2, Build time: 17-January-2013 10:26:34 */ (function(){ var EventTarget = parserlib.util.EventTarget, TokenStreamBase = parserlib.util.TokenStreamBase, StringReader = parserlib.util.StringReader, SyntaxError = parserlib.util.SyntaxError, SyntaxUnit = parserlib.util.SyntaxUnit; var Colors = { aliceblue :"#f0f8ff", antiquewhite :"#faebd7", aqua :"#00ffff", aquamarine :"#7fffd4", azure :"#f0ffff", beige :"#f5f5dc", bisque :"#ffe4c4", black :"#000000", blanchedalmond :"#ffebcd", blue :"#0000ff", blueviolet :"#8a2be2", brown :"#a52a2a", burlywood :"#deb887", cadetblue :"#5f9ea0", chartreuse :"#7fff00", chocolate :"#d2691e", coral :"#ff7f50", cornflowerblue :"#6495ed", cornsilk :"#fff8dc", crimson :"#dc143c", cyan :"#00ffff", darkblue :"#00008b", darkcyan :"#008b8b", darkgoldenrod :"#b8860b", darkgray :"#a9a9a9", darkgreen :"#006400", darkkhaki :"#bdb76b", darkmagenta :"#8b008b", darkolivegreen :"#556b2f", darkorange :"#ff8c00", darkorchid :"#9932cc", darkred :"#8b0000", darksalmon :"#e9967a", darkseagreen :"#8fbc8f", darkslateblue :"#483d8b", darkslategray :"#2f4f4f", darkturquoise :"#00ced1", darkviolet :"#9400d3", deeppink :"#ff1493", deepskyblue :"#00bfff", dimgray :"#696969", dodgerblue :"#1e90ff", firebrick :"#b22222", floralwhite :"#fffaf0", forestgreen :"#228b22", fuchsia :"#ff00ff", gainsboro :"#dcdcdc", ghostwhite :"#f8f8ff", gold :"#ffd700", goldenrod :"#daa520", gray :"#808080", green :"#008000", greenyellow :"#adff2f", honeydew :"#f0fff0", hotpink :"#ff69b4", indianred :"#cd5c5c", indigo :"#4b0082", ivory :"#fffff0", khaki :"#f0e68c", lavender :"#e6e6fa", lavenderblush :"#fff0f5", lawngreen :"#7cfc00", lemonchiffon :"#fffacd", lightblue :"#add8e6", lightcoral :"#f08080", lightcyan :"#e0ffff", lightgoldenrodyellow :"#fafad2", lightgray :"#d3d3d3", lightgreen :"#90ee90", lightpink :"#ffb6c1", lightsalmon :"#ffa07a", lightseagreen :"#20b2aa", lightskyblue :"#87cefa", lightslategray :"#778899", lightsteelblue :"#b0c4de", lightyellow :"#ffffe0", lime :"#00ff00", limegreen :"#32cd32", linen :"#faf0e6", magenta :"#ff00ff", maroon :"#800000", mediumaquamarine:"#66cdaa", mediumblue :"#0000cd", mediumorchid :"#ba55d3", mediumpurple :"#9370d8", mediumseagreen :"#3cb371", mediumslateblue :"#7b68ee", mediumspringgreen :"#00fa9a", mediumturquoise :"#48d1cc", mediumvioletred :"#c71585", midnightblue :"#191970", mintcream :"#f5fffa", mistyrose :"#ffe4e1", moccasin :"#ffe4b5", navajowhite :"#ffdead", navy :"#000080", oldlace :"#fdf5e6", olive :"#808000", olivedrab :"#6b8e23", orange :"#ffa500", orangered :"#ff4500", orchid :"#da70d6", palegoldenrod :"#eee8aa", palegreen :"#98fb98", paleturquoise :"#afeeee", palevioletred :"#d87093", papayawhip :"#ffefd5", peachpuff :"#ffdab9", peru :"#cd853f", pink :"#ffc0cb", plum :"#dda0dd", powderblue :"#b0e0e6", purple :"#800080", red :"#ff0000", rosybrown :"#bc8f8f", royalblue :"#4169e1", saddlebrown :"#8b4513", salmon :"#fa8072", sandybrown :"#f4a460", seagreen :"#2e8b57", seashell :"#fff5ee", sienna :"#a0522d", silver :"#c0c0c0", skyblue :"#87ceeb", slateblue :"#6a5acd", slategray :"#708090", snow :"#fffafa", springgreen :"#00ff7f", steelblue :"#4682b4", tan :"#d2b48c", teal :"#008080", thistle :"#d8bfd8", tomato :"#ff6347", turquoise :"#40e0d0", violet :"#ee82ee", wheat :"#f5deb3", white :"#ffffff", whitesmoke :"#f5f5f5", yellow :"#ffff00", yellowgreen :"#9acd32", //CSS2 system colors http://www.w3.org/TR/css3-color/#css2-system activeBorder :"Active window border.", activecaption :"Active window caption.", appworkspace :"Background color of multiple document interface.", background :"Desktop background.", buttonface :"The face background color for 3-D elements that appear 3-D due to one layer of surrounding border.", buttonhighlight :"The color of the border facing the light source for 3-D elements that appear 3-D due to one layer of surrounding border.", buttonshadow :"The color of the border away from the light source for 3-D elements that appear 3-D due to one layer of surrounding border.", buttontext :"Text on push buttons.", captiontext :"Text in caption, size box, and scrollbar arrow box.", graytext :"Grayed (disabled) text. This color is set to #000 if the current display driver does not support a solid gray color.", highlight :"Item(s) selected in a control.", highlighttext :"Text of item(s) selected in a control.", inactiveborder :"Inactive window border.", inactivecaption :"Inactive window caption.", inactivecaptiontext :"Color of text in an inactive caption.", infobackground :"Background color for tooltip controls.", infotext :"Text color for tooltip controls.", menu :"Menu background.", menutext :"Text in menus.", scrollbar :"Scroll bar gray area.", threeddarkshadow :"The color of the darker (generally outer) of the two borders away from the light source for 3-D elements that appear 3-D due to two concentric layers of surrounding border.", threedface :"The face background color for 3-D elements that appear 3-D due to two concentric layers of surrounding border.", threedhighlight :"The color of the lighter (generally outer) of the two borders facing the light source for 3-D elements that appear 3-D due to two concentric layers of surrounding border.", threedlightshadow :"The color of the darker (generally inner) of the two borders facing the light source for 3-D elements that appear 3-D due to two concentric layers of surrounding border.", threedshadow :"The color of the lighter (generally inner) of the two borders away from the light source for 3-D elements that appear 3-D due to two concentric layers of surrounding border.", window :"Window background.", windowframe :"Window frame.", windowtext :"Text in windows." }; /*global SyntaxUnit, Parser*/ /** * Represents a selector combinator (whitespace, +, >). * @namespace parserlib.css * @class Combinator * @extends parserlib.util.SyntaxUnit * @constructor * @param {String} text The text representation of the unit. * @param {int} line The line of text on which the unit resides. * @param {int} col The column of text on which the unit resides. */ function Combinator(text, line, col){ SyntaxUnit.call(this, text, line, col, Parser.COMBINATOR_TYPE); /** * The type of modifier. * @type String * @property type */ this.type = "unknown"; //pretty simple if (/^\s+$/.test(text)){ this.type = "descendant"; } else if (text == ">"){ this.type = "child"; } else if (text == "+"){ this.type = "adjacent-sibling"; } else if (text == "~"){ this.type = "sibling"; } } Combinator.prototype = new SyntaxUnit(); Combinator.prototype.constructor = Combinator; /*global SyntaxUnit, Parser*/ /** * Represents a media feature, such as max-width:500. * @namespace parserlib.css * @class MediaFeature * @extends parserlib.util.SyntaxUnit * @constructor * @param {SyntaxUnit} name The name of the feature. * @param {SyntaxUnit} value The value of the feature or null if none. */ function MediaFeature(name, value){ SyntaxUnit.call(this, "(" + name + (value !== null ? ":" + value : "") + ")", name.startLine, name.startCol, Parser.MEDIA_FEATURE_TYPE); /** * The name of the media feature * @type String * @property name */ this.name = name; /** * The value for the feature or null if there is none. * @type SyntaxUnit * @property value */ this.value = value; } MediaFeature.prototype = new SyntaxUnit(); MediaFeature.prototype.constructor = MediaFeature; /*global SyntaxUnit, Parser*/ /** * Represents an individual media query. * @namespace parserlib.css * @class MediaQuery * @extends parserlib.util.SyntaxUnit * @constructor * @param {String} modifier The modifier "not" or "only" (or null). * @param {String} mediaType The type of media (i.e., "print"). * @param {Array} parts Array of selectors parts making up this selector. * @param {int} line The line of text on which the unit resides. * @param {int} col The column of text on which the unit resides. */ function MediaQuery(modifier, mediaType, features, line, col){ SyntaxUnit.call(this, (modifier ? modifier + " ": "") + (mediaType ? mediaType : "") + (mediaType && features.length > 0 ? " and " : "") + features.join(" and "), line, col, Parser.MEDIA_QUERY_TYPE); /** * The media modifier ("not" or "only") * @type String * @property modifier */ this.modifier = modifier; /** * The mediaType (i.e., "print") * @type String * @property mediaType */ this.mediaType = mediaType; /** * The parts that make up the selector. * @type Array * @property features */ this.features = features; } MediaQuery.prototype = new SyntaxUnit(); MediaQuery.prototype.constructor = MediaQuery; /*global Tokens, TokenStream, SyntaxError, Properties, Validation, ValidationError, SyntaxUnit, PropertyValue, PropertyValuePart, SelectorPart, SelectorSubPart, Selector, PropertyName, Combinator, MediaFeature, MediaQuery, EventTarget */ /** * A CSS3 parser. * @namespace parserlib.css * @class Parser * @constructor * @param {Object} options (Optional) Various options for the parser: * starHack (true|false) to allow IE6 star hack as valid, * underscoreHack (true|false) to interpret leading underscores * as IE6-7 targeting for known properties, ieFilters (true|false) * to indicate that IE < 8 filters should be accepted and not throw * syntax errors. */ function Parser(options){ //inherit event functionality EventTarget.call(this); this.options = options || {}; this._tokenStream = null; } //Static constants Parser.DEFAULT_TYPE = 0; Parser.COMBINATOR_TYPE = 1; Parser.MEDIA_FEATURE_TYPE = 2; Parser.MEDIA_QUERY_TYPE = 3; Parser.PROPERTY_NAME_TYPE = 4; Parser.PROPERTY_VALUE_TYPE = 5; Parser.PROPERTY_VALUE_PART_TYPE = 6; Parser.SELECTOR_TYPE = 7; Parser.SELECTOR_PART_TYPE = 8; Parser.SELECTOR_SUB_PART_TYPE = 9; Parser.prototype = function(){ var proto = new EventTarget(), //new prototype prop, additions = { //restore constructor constructor: Parser, //instance constants - yuck DEFAULT_TYPE : 0, COMBINATOR_TYPE : 1, MEDIA_FEATURE_TYPE : 2, MEDIA_QUERY_TYPE : 3, PROPERTY_NAME_TYPE : 4, PROPERTY_VALUE_TYPE : 5, PROPERTY_VALUE_PART_TYPE : 6, SELECTOR_TYPE : 7, SELECTOR_PART_TYPE : 8, SELECTOR_SUB_PART_TYPE : 9, //----------------------------------------------------------------- // Grammar //----------------------------------------------------------------- _stylesheet: function(){ /* * stylesheet * : [ CHARSET_SYM S* STRING S* ';' ]? * [S|CDO|CDC]* [ import [S|CDO|CDC]* ]* * [ namespace [S|CDO|CDC]* ]* * [ [ ruleset | media | page | font_face | keyframes ] [S|CDO|CDC]* ]* * ; */ var tokenStream = this._tokenStream, charset = null, count, token, tt; this.fire("startstylesheet"); //try to read character set this._charset(); this._skipCruft(); //try to read imports - may be more than one while (tokenStream.peek() == Tokens.IMPORT_SYM){ this._import(); this._skipCruft(); } //try to read namespaces - may be more than one while (tokenStream.peek() == Tokens.NAMESPACE_SYM){ this._namespace(); this._skipCruft(); } //get the next token tt = tokenStream.peek(); //try to read the rest while(tt > Tokens.EOF){ try { switch(tt){ case Tokens.MEDIA_SYM: this._media(); this._skipCruft(); break; case Tokens.PAGE_SYM: this._page(); this._skipCruft(); break; case Tokens.FONT_FACE_SYM: this._font_face(); this._skipCruft(); break; case Tokens.KEYFRAMES_SYM: this._keyframes(); this._skipCruft(); break; case Tokens.UNKNOWN_SYM: //unknown @ rule tokenStream.get(); if (!this.options.strict){ //fire error event this.fire({ type: "error", error: null, message: "Unknown @ rule: " + tokenStream.LT(0).value + ".", line: tokenStream.LT(0).startLine, col: tokenStream.LT(0).startCol }); //skip braces count=0; while (tokenStream.advance([Tokens.LBRACE, Tokens.RBRACE]) == Tokens.LBRACE){ count++; //keep track of nesting depth } while(count){ tokenStream.advance([Tokens.RBRACE]); count--; } } else { //not a syntax error, rethrow it throw new SyntaxError("Unknown @ rule.", tokenStream.LT(0).startLine, tokenStream.LT(0).startCol); } break; case Tokens.S: this._readWhitespace(); break; default: if(!this._ruleset()){ //error handling for known issues switch(tt){ case Tokens.CHARSET_SYM: token = tokenStream.LT(1); this._charset(false); throw new SyntaxError("@charset not allowed here.", token.startLine, token.startCol); case Tokens.IMPORT_SYM: token = tokenStream.LT(1); this._import(false); throw new SyntaxError("@import not allowed here.", token.startLine, token.startCol); case Tokens.NAMESPACE_SYM: token = tokenStream.LT(1); this._namespace(false); throw new SyntaxError("@namespace not allowed here.", token.startLine, token.startCol); default: tokenStream.get(); //get the last token this._unexpectedToken(tokenStream.token()); } } } } catch(ex) { if (ex instanceof SyntaxError && !this.options.strict){ this.fire({ type: "error", error: ex, message: ex.message, line: ex.line, col: ex.col }); } else { throw ex; } } tt = tokenStream.peek(); } if (tt != Tokens.EOF){ this._unexpectedToken(tokenStream.token()); } this.fire("endstylesheet"); }, _charset: function(emit){ var tokenStream = this._tokenStream, charset, token, line, col; if (tokenStream.match(Tokens.CHARSET_SYM)){ line = tokenStream.token().startLine; col = tokenStream.token().startCol; this._readWhitespace(); tokenStream.mustMatch(Tokens.STRING); token = tokenStream.token(); charset = token.value; this._readWhitespace(); tokenStream.mustMatch(Tokens.SEMICOLON); if (emit !== false){ this.fire({ type: "charset", charset:charset, line: line, col: col }); } } }, _import: function(emit){ /* * import * : IMPORT_SYM S* * [STRING|URI] S* media_query_list? ';' S* */ var tokenStream = this._tokenStream, tt, uri, importToken, mediaList = []; //read import symbol tokenStream.mustMatch(Tokens.IMPORT_SYM); importToken = tokenStream.token(); this._readWhitespace(); tokenStream.mustMatch([Tokens.STRING, Tokens.URI]); //grab the URI value uri = tokenStream.token().value.replace(/(?:url\()?["']([^"']+)["']\)?/, "$1"); this._readWhitespace(); mediaList = this._media_query_list(); //must end with a semicolon tokenStream.mustMatch(Tokens.SEMICOLON); this._readWhitespace(); if (emit !== false){ this.fire({ type: "import", uri: uri, media: mediaList, line: importToken.startLine, col: importToken.startCol }); } }, _namespace: function(emit){ /* * namespace * : NAMESPACE_SYM S* [namespace_prefix S*]? [STRING|URI] S* ';' S* */ var tokenStream = this._tokenStream, line, col, prefix, uri; //read import symbol tokenStream.mustMatch(Tokens.NAMESPACE_SYM); line = tokenStream.token().startLine; col = tokenStream.token().startCol; this._readWhitespace(); //it's a namespace prefix - no _namespace_prefix() method because it's just an IDENT if (tokenStream.match(Tokens.IDENT)){ prefix = tokenStream.token().value; this._readWhitespace(); } tokenStream.mustMatch([Tokens.STRING, Tokens.URI]); /*if (!tokenStream.match(Tokens.STRING)){ tokenStream.mustMatch(Tokens.URI); }*/ //grab the URI value uri = tokenStream.token().value.replace(/(?:url\()?["']([^"']+)["']\)?/, "$1"); this._readWhitespace(); //must end with a semicolon tokenStream.mustMatch(Tokens.SEMICOLON); this._readWhitespace(); if (emit !== false){ this.fire({ type: "namespace", prefix: prefix, uri: uri, line: line, col: col }); } }, _media: function(){ /* * media * : MEDIA_SYM S* media_query_list S* '{' S* ruleset* '}' S* * ; */ var tokenStream = this._tokenStream, line, col, mediaList;// = []; //look for @media tokenStream.mustMatch(Tokens.MEDIA_SYM); line = tokenStream.token().startLine; col = tokenStream.token().startCol; this._readWhitespace(); mediaList = this._media_query_list(); tokenStream.mustMatch(Tokens.LBRACE); this._readWhitespace(); this.fire({ type: "startmedia", media: mediaList, line: line, col: col }); while(true) { if (tokenStream.peek() == Tokens.PAGE_SYM){ this._page(); } else if (!this._ruleset()){ break; } } tokenStream.mustMatch(Tokens.RBRACE); this._readWhitespace(); this.fire({ type: "endmedia", media: mediaList, line: line, col: col }); }, //CSS3 Media Queries _media_query_list: function(){ /* * media_query_list * : S* [media_query [ ',' S* media_query ]* ]? * ; */ var tokenStream = this._tokenStream, mediaList = []; this._readWhitespace(); if (tokenStream.peek() == Tokens.IDENT || tokenStream.peek() == Tokens.LPAREN){ mediaList.push(this._media_query()); } while(tokenStream.match(Tokens.COMMA)){ this._readWhitespace(); mediaList.push(this._media_query()); } return mediaList; }, /* * Note: "expression" in the grammar maps to the _media_expression * method. */ _media_query: function(){ /* * media_query * : [ONLY | NOT]? S* media_type S* [ AND S* expression ]* * | expression [ AND S* expression ]* * ; */ var tokenStream = this._tokenStream, type = null, ident = null, token = null, expressions = []; if (tokenStream.match(Tokens.IDENT)){ ident = tokenStream.token().value.toLowerCase(); //since there's no custom tokens for these, need to manually check if (ident != "only" && ident != "not"){ tokenStream.unget(); ident = null; } else { token = tokenStream.token(); } } this._readWhitespace(); if (tokenStream.peek() == Tokens.IDENT){ type = this._media_type(); if (token === null){ token = tokenStream.token(); } } else if (tokenStream.peek() == Tokens.LPAREN){ if (token === null){ token = tokenStream.LT(1); } expressions.push(this._media_expression()); } if (type === null && expressions.length === 0){ return null; } else { this._readWhitespace(); while (tokenStream.match(Tokens.IDENT)){ if (tokenStream.token().value.toLowerCase() != "and"){ this._unexpectedToken(tokenStream.token()); } this._readWhitespace(); expressions.push(this._media_expression()); } } return new MediaQuery(ident, type, expressions, token.startLine, token.startCol); }, //CSS3 Media Queries _media_type: function(){ /* * media_type * : IDENT * ; */ return this._media_feature(); }, /** * Note: in CSS3 Media Queries, this is called "expression". * Renamed here to avoid conflict with CSS3 Selectors * definition of "expression". Also note that "expr" in the * grammar now maps to "expression" from CSS3 selectors. * @method _media_expression * @private */ _media_expression: function(){ /* * expression * : '(' S* media_feature S* [ ':' S* expr ]? ')' S* * ; */ var tokenStream = this._tokenStream, feature = null, token, expression = null; tokenStream.mustMatch(Tokens.LPAREN); feature = this._media_feature(); this._readWhitespace(); if (tokenStream.match(Tokens.COLON)){ this._readWhitespace(); token = tokenStream.LT(1); expression = this._expression(); } tokenStream.mustMatch(Tokens.RPAREN); this._readWhitespace(); return new MediaFeature(feature, (expression ? new SyntaxUnit(expression, token.startLine, token.startCol) : null)); }, //CSS3 Media Queries _media_feature: function(){ /* * media_feature * : IDENT * ; */ var tokenStream = this._tokenStream; tokenStream.mustMatch(Tokens.IDENT); return SyntaxUnit.fromToken(tokenStream.token()); }, //CSS3 Paged Media _page: function(){ /* * page: * PAGE_SYM S* IDENT? pseudo_page? S* * '{' S* [ declaration | margin ]? [ ';' S* [ declaration | margin ]? ]* '}' S* * ; */ var tokenStream = this._tokenStream, line, col, identifier = null, pseudoPage = null; //look for @page tokenStream.mustMatch(Tokens.PAGE_SYM); line = tokenStream.token().startLine; col = tokenStream.token().startCol; this._readWhitespace(); if (tokenStream.match(Tokens.IDENT)){ identifier = tokenStream.token().value; //The value 'auto' may not be used as a page name and MUST be treated as a syntax error. if (identifier.toLowerCase() === "auto"){ this._unexpectedToken(tokenStream.token()); } } //see if there's a colon upcoming if (tokenStream.peek() == Tokens.COLON){ pseudoPage = this._pseudo_page(); } this._readWhitespace(); this.fire({ type: "startpage", id: identifier, pseudo: pseudoPage, line: line, col: col }); this._readDeclarations(true, true); this.fire({ type: "endpage", id: identifier, pseudo: pseudoPage, line: line, col: col }); }, //CSS3 Paged Media _margin: function(){ /* * margin : * margin_sym S* '{' declaration [ ';' S* declaration? ]* '}' S* * ; */ var tokenStream = this._tokenStream, line, col, marginSym = this._margin_sym(); if (marginSym){ line = tokenStream.token().startLine; col = tokenStream.token().startCol; this.fire({ type: "startpagemargin", margin: marginSym, line: line, col: col }); this._readDeclarations(true); this.fire({ type: "endpagemargin", margin: marginSym, line: line, col: col }); return true; } else { return false; } }, //CSS3 Paged Media _margin_sym: function(){ /* * margin_sym : * TOPLEFTCORNER_SYM | * TOPLEFT_SYM | * TOPCENTER_SYM | * TOPRIGHT_SYM | * TOPRIGHTCORNER_SYM | * BOTTOMLEFTCORNER_SYM | * BOTTOMLEFT_SYM | * BOTTOMCENTER_SYM | * BOTTOMRIGHT_SYM | * BOTTOMRIGHTCORNER_SYM | * LEFTTOP_SYM | * LEFTMIDDLE_SYM | * LEFTBOTTOM_SYM | * RIGHTTOP_SYM | * RIGHTMIDDLE_SYM | * RIGHTBOTTOM_SYM * ; */ var tokenStream = this._tokenStream; if(tokenStream.match([Tokens.TOPLEFTCORNER_SYM, Tokens.TOPLEFT_SYM, Tokens.TOPCENTER_SYM, Tokens.TOPRIGHT_SYM, Tokens.TOPRIGHTCORNER_SYM, Tokens.BOTTOMLEFTCORNER_SYM, Tokens.BOTTOMLEFT_SYM, Tokens.BOTTOMCENTER_SYM, Tokens.BOTTOMRIGHT_SYM, Tokens.BOTTOMRIGHTCORNER_SYM, Tokens.LEFTTOP_SYM, Tokens.LEFTMIDDLE_SYM, Tokens.LEFTBOTTOM_SYM, Tokens.RIGHTTOP_SYM, Tokens.RIGHTMIDDLE_SYM, Tokens.RIGHTBOTTOM_SYM])) { return SyntaxUnit.fromToken(tokenStream.token()); } else { return null; } }, _pseudo_page: function(){ /* * pseudo_page * : ':' IDENT * ; */ var tokenStream = this._tokenStream; tokenStream.mustMatch(Tokens.COLON); tokenStream.mustMatch(Tokens.IDENT); //TODO: CSS3 Paged Media says only "left", "center", and "right" are allowed return tokenStream.token().value; }, _font_face: function(){ /* * font_face * : FONT_FACE_SYM S* * '{' S* declaration [ ';' S* declaration ]* '}' S* * ; */ var tokenStream = this._tokenStream, line, col; //look for @page tokenStream.mustMatch(Tokens.FONT_FACE_SYM); line = tokenStream.token().startLine; col = tokenStream.token().startCol; this._readWhitespace(); this.fire({ type: "startfontface", line: line, col: col }); this._readDeclarations(true); this.fire({ type: "endfontface", line: line, col: col }); }, _operator: function(inFunction){ /* * operator (outside function) * : '/' S* | ',' S* | /( empty )/ * operator (inside function) * : '/' S* | '+' S* | '*' S* | '-' S* /( empty )/ * ; */ var tokenStream = this._tokenStream, token = null; if (tokenStream.match([Tokens.SLASH, Tokens.COMMA]) || (inFunction && tokenStream.match([Tokens.PLUS, Tokens.STAR, Tokens.MINUS]))){ token = tokenStream.token(); this._readWhitespace(); } return token ? PropertyValuePart.fromToken(token) : null; }, _combinator: function(){ /* * combinator * : PLUS S* | GREATER S* | TILDE S* | S+ * ; */ var tokenStream = this._tokenStream, value = null, token; if(tokenStream.match([Tokens.PLUS, Tokens.GREATER, Tokens.TILDE])){ token = tokenStream.token(); value = new Combinator(token.value, token.startLine, token.startCol); this._readWhitespace(); } return value; }, _unary_operator: function(){ /* * unary_operator * : '-' | '+' * ; */ var tokenStream = this._tokenStream; if (tokenStream.match([Tokens.MINUS, Tokens.PLUS])){ return tokenStream.token().value; } else { return null; } }, _property: function(){ /* * property * : IDENT S* * ; */ var tokenStream = this._tokenStream, value = null, hack = null, tokenValue, token, line, col; //check for star hack - throws error if not allowed if (tokenStream.peek() == Tokens.STAR && this.options.starHack){ tokenStream.get(); token = tokenStream.token(); hack = token.value; line = token.startLine; col = token.startCol; } if(tokenStream.match(Tokens.IDENT)){ token = tokenStream.token(); tokenValue = token.value; //check for underscore hack - no error if not allowed because it's valid CSS syntax if (tokenValue.charAt(0) == "_" && this.options.underscoreHack){ hack = "_"; tokenValue = tokenValue.substring(1); } value = new PropertyName(tokenValue, hack, (line||token.startLine), (col||token.startCol)); this._readWhitespace(); } return value; }, //Augmented with CSS3 Selectors _ruleset: function(){ /* * ruleset * : selectors_group * '{' S* declaration? [ ';' S* declaration? ]* '}' S* * ; */ var tokenStream = this._tokenStream, tt, selectors; /* * Error Recovery: If even a single selector fails to parse, * then the entire ruleset should be thrown away. */ try { selectors = this._selectors_group(); } catch (ex){ if (ex instanceof SyntaxError && !this.options.strict){ //fire error event this.fire({ type: "error", error: ex, message: ex.message, line: ex.line, col: ex.col }); //skip over everything until closing brace tt = tokenStream.advance([Tokens.RBRACE]); if (tt == Tokens.RBRACE){ //if there's a right brace, the rule is finished so don't do anything } else { //otherwise, rethrow the error because it wasn't handled properly throw ex; } } else { //not a syntax error, rethrow it throw ex; } //trigger parser to continue return true; } //if it got here, all selectors parsed if (selectors){ this.fire({ type: "startrule", selectors: selectors, line: selectors[0].line, col: selectors[0].col }); this._readDeclarations(true); this.fire({ type: "endrule", selectors: selectors, line: selectors[0].line, col: selectors[0].col }); } return selectors; }, //CSS3 Selectors _selectors_group: function(){ /* * selectors_group * : selector [ COMMA S* selector ]* * ; */ var tokenStream = this._tokenStream, selectors = [], selector; selector = this._selector(); if (selector !== null){ selectors.push(selector); while(tokenStream.match(Tokens.COMMA)){ this._readWhitespace(); selector = this._selector(); if (selector !== null){ selectors.push(selector); } else { this._unexpectedToken(tokenStream.LT(1)); } } } return selectors.length ? selectors : null; }, //CSS3 Selectors _selector: function(){ /* * selector * : simple_selector_sequence [ combinator simple_selector_sequence ]* * ; */ var tokenStream = this._tokenStream, selector = [], nextSelector = null, combinator = null, ws = null; //if there's no simple selector, then there's no selector nextSelector = this._simple_selector_sequence(); if (nextSelector === null){ return null; } selector.push(nextSelector); do { //look for a combinator combinator = this._combinator(); if (combinator !== null){ selector.push(combinator); nextSelector = this._simple_selector_sequence(); //there must be a next selector if (nextSelector === null){ this._unexpectedToken(tokenStream.LT(1)); } else { //nextSelector is an instance of SelectorPart selector.push(nextSelector); } } else { //if there's not whitespace, we're done if (this._readWhitespace()){ //add whitespace separator ws = new Combinator(tokenStream.token().value, tokenStream.token().startLine, tokenStream.token().startCol); //combinator is not required combinator = this._combinator(); //selector is required if there's a combinator nextSelector = this._simple_selector_sequence(); if (nextSelector === null){ if (combinator !== null){ this._unexpectedToken(tokenStream.LT(1)); } } else { if (combinator !== null){ selector.push(combinator); } else { selector.push(ws); } selector.push(nextSelector); } } else { break; } } } while(true); return new Selector(selector, selector[0].line, selector[0].col); }, //CSS3 Selectors _simple_selector_sequence: function(){ /* * simple_selector_sequence * : [ type_selector | universal ] * [ HASH | class | attrib | pseudo | negation ]* * | [ HASH | class | attrib | pseudo | negation ]+ * ; */ var tokenStream = this._tokenStream, //parts of a simple selector elementName = null, modifiers = [], //complete selector text selectorText= "", //the different parts after the element name to search for components = [ //HASH function(){ return tokenStream.match(Tokens.HASH) ? new SelectorSubPart(tokenStream.token().value, "id", tokenStream.token().startLine, tokenStream.token().startCol) : null; }, this._class, this._attrib, this._pseudo, this._negation ], i = 0, len = components.length, component = null, found = false, line, col; //get starting line and column for the selector line = tokenStream.LT(1).startLine; col = tokenStream.LT(1).startCol; elementName = this._type_selector(); if (!elementName){ elementName = this._universal(); } if (elementName !== null){ selectorText += elementName; } while(true){ //whitespace means we're done if (tokenStream.peek() === Tokens.S){ break; } //check for each component while(i < len && component === null){ component = components[i++].call(this); } if (component === null){ //we don't have a selector if (selectorText === ""){ return null; } else { break; } } else { i = 0; modifiers.push(component); selectorText += component.toString(); component = null; } } return selectorText !== "" ? new SelectorPart(elementName, modifiers, selectorText, line, col) : null; }, //CSS3 Selectors _type_selector: function(){ /* * type_selector * : [ namespace_prefix ]? element_name * ; */ var tokenStream = this._tokenStream, ns = this._namespace_prefix(), elementName = this._element_name(); if (!elementName){ /* * Need to back out the namespace that was read due to both * type_selector and universal reading namespace_prefix * first. Kind of hacky, but only way I can figure out * right now how to not change the grammar. */ if (ns){ tokenStream.unget(); if (ns.length > 1){ tokenStream.unget(); } } return null; } else { if (ns){ elementName.text = ns + elementName.text; elementName.col -= ns.length; } return elementName; } }, //CSS3 Selectors _class: function(){ /* * class * : '.' IDENT * ; */ var tokenStream = this._tokenStream, token; if (tokenStream.match(Tokens.DOT)){ tokenStream.mustMatch(Tokens.IDENT); token = tokenStream.token(); return new SelectorSubPart("." + token.value, "class", token.startLine, token.startCol - 1); } else { return null; } }, //CSS3 Selectors _element_name: function(){ /* * element_name * : IDENT * ; */ var tokenStream = this._tokenStream, token; if (tokenStream.match(Tokens.IDENT)){ token = tokenStream.token(); return new SelectorSubPart(token.value, "elementName", token.startLine, token.startCol); } else { return null; } }, //CSS3 Selectors _namespace_prefix: function(){ /* * namespace_prefix * : [ IDENT | '*' ]? '|' * ; */ var tokenStream = this._tokenStream, value = ""; //verify that this is a namespace prefix if (tokenStream.LA(1) === Tokens.PIPE || tokenStream.LA(2) === Tokens.PIPE){ if(tokenStream.match([Tokens.IDENT, Tokens.STAR])){ value += tokenStream.token().value; } tokenStream.mustMatch(Tokens.PIPE); value += "|"; } return value.length ? value : null; }, //CSS3 Selectors _universal: function(){ /* * universal * : [ namespace_prefix ]? '*' * ; */ var tokenStream = this._tokenStream, value = "", ns; ns = this._namespace_prefix(); if(ns){ value += ns; } if(tokenStream.match(Tokens.STAR)){ value += "*"; } return value.length ? value : null; }, //CSS3 Selectors _attrib: function(){ /* * attrib * : '[' S* [ namespace_prefix ]? IDENT S* * [ [ PREFIXMATCH | * SUFFIXMATCH | * SUBSTRINGMATCH | * '=' | * INCLUDES | * DASHMATCH ] S* [ IDENT | STRING ] S* * ]? ']' * ; */ var tokenStream = this._tokenStream, value = null, ns, token; if (tokenStream.match(Tokens.LBRACKET)){ token = tokenStream.token(); value = token.value; value += this._readWhitespace(); ns = this._namespace_prefix(); if (ns){ value += ns; } tokenStream.mustMatch(Tokens.IDENT); value += tokenStream.token().value; value += this._readWhitespace(); if(tokenStream.match([Tokens.PREFIXMATCH, Tokens.SUFFIXMATCH, Tokens.SUBSTRINGMATCH, Tokens.EQUALS, Tokens.INCLUDES, Tokens.DASHMATCH])){ value += tokenStream.token().value; value += this._readWhitespace(); tokenStream.mustMatch([Tokens.IDENT, Tokens.STRING]); value += tokenStream.token().value; value += this._readWhitespace(); } tokenStream.mustMatch(Tokens.RBRACKET); return new SelectorSubPart(value + "]", "attribute", token.startLine, token.startCol); } else { return null; } }, //CSS3 Selectors _pseudo: function(){ /* * pseudo * : ':' ':'? [ IDENT | functional_pseudo ] * ; */ var tokenStream = this._tokenStream, pseudo = null, colons = ":", line, col; if (tokenStream.match(Tokens.COLON)){ if (tokenStream.match(Tokens.COLON)){ colons += ":"; } if (tokenStream.match(Tokens.IDENT)){ pseudo = tokenStream.token().value; line = tokenStream.token().startLine; col = tokenStream.token().startCol - colons.length; } else if (tokenStream.peek() == Tokens.FUNCTION){ line = tokenStream.LT(1).startLine; col = tokenStream.LT(1).startCol - colons.length; pseudo = this._functional_pseudo(); } if (pseudo){ pseudo = new SelectorSubPart(colons + pseudo, "pseudo", line, col); } } return pseudo; }, //CSS3 Selectors _functional_pseudo: function(){ /* * functional_pseudo * : FUNCTION S* expression ')' * ; */ var tokenStream = this._tokenStream, value = null; if(tokenStream.match(Tokens.FUNCTION)){ value = tokenStream.token().value; value += this._readWhitespace(); value += this._expression(); tokenStream.mustMatch(Tokens.RPAREN); value += ")"; } return value; }, //CSS3 Selectors _expression: function(){ /* * expression * : [ [ PLUS | '-' | DIMENSION | NUMBER | STRING | IDENT ] S* ]+ * ; */ var tokenStream = this._tokenStream, value = ""; while(tokenStream.match([Tokens.PLUS, Tokens.MINUS, Tokens.DIMENSION, Tokens.NUMBER, Tokens.STRING, Tokens.IDENT, Tokens.LENGTH, Tokens.FREQ, Tokens.ANGLE, Tokens.TIME, Tokens.RESOLUTION, Tokens.SLASH])){ value += tokenStream.token().value; value += this._readWhitespace(); } return value.length ? value : null; }, //CSS3 Selectors _negation: function(){ /* * negation * : NOT S* negation_arg S* ')' * ; */ var tokenStream = this._tokenStream, line, col, value = "", arg, subpart = null; if (tokenStream.match(Tokens.NOT)){ value = tokenStream.token().value; line = tokenStream.token().startLine; col = tokenStream.token().startCol; value += this._readWhitespace(); arg = this._negation_arg(); value += arg; value += this._readWhitespace(); tokenStream.match(Tokens.RPAREN); value += tokenStream.token().value; subpart = new SelectorSubPart(value, "not", line, col); subpart.args.push(arg); } return subpart; }, //CSS3 Selectors _negation_arg: function(){ /* * negation_arg * : type_selector | universal | HASH | class | attrib | pseudo * ; */ var tokenStream = this._tokenStream, args = [ this._type_selector, this._universal, function(){ return tokenStream.match(Tokens.HASH) ? new SelectorSubPart(tokenStream.token().value, "id", tokenStream.token().startLine, tokenStream.token().startCol) : null; }, this._class, this._attrib, this._pseudo ], arg = null, i = 0, len = args.length, elementName, line, col, part; line = tokenStream.LT(1).startLine; col = tokenStream.LT(1).startCol; while(i < len && arg === null){ arg = args[i].call(this); i++; } //must be a negation arg if (arg === null){ this._unexpectedToken(tokenStream.LT(1)); } //it's an element name if (arg.type == "elementName"){ part = new SelectorPart(arg, [], arg.toString(), line, col); } else { part = new SelectorPart(null, [arg], arg.toString(), line, col); } return part; }, _declaration: function(){ /* * declaration * : property ':' S* expr prio? * | /( empty )/ * ; */ var tokenStream = this._tokenStream, property = null, expr = null, prio = null, error = null, invalid = null, propertyName= ""; property = this._property(); if (property !== null){ tokenStream.mustMatch(Tokens.COLON); this._readWhitespace(); expr = this._expr(); //if there's no parts for the value, it's an error if (!expr || expr.length === 0){ this._unexpectedToken(tokenStream.LT(1)); } prio = this._prio(); /* * If hacks should be allowed, then only check the root * property. If hacks should not be allowed, treat * _property or *property as invalid properties. */ propertyName = property.toString(); if (this.options.starHack && property.hack == "*" || this.options.underscoreHack && property.hack == "_") { propertyName = property.text; } try { this._validateProperty(propertyName, expr); } catch (ex) { invalid = ex; } this.fire({ type: "property", property: property, value: expr, important: prio, line: property.line, col: property.col, invalid: invalid }); return true; } else { return false; } }, _prio: function(){ /* * prio * : IMPORTANT_SYM S* * ; */ var tokenStream = this._tokenStream, result = tokenStream.match(Tokens.IMPORTANT_SYM); this._readWhitespace(); return result; }, _expr: function(inFunction){ /* * expr * : term [ operator term ]* * ; */ var tokenStream = this._tokenStream, values = [], //valueParts = [], value = null, operator = null; value = this._term(); if (value !== null){ values.push(value); do { operator = this._operator(inFunction); //if there's an operator, keep building up the value parts if (operator){ values.push(operator); } /*else { //if there's not an operator, you have a full value values.push(new PropertyValue(valueParts, valueParts[0].line, valueParts[0].col)); valueParts = []; }*/ value = this._term(); if (value === null){ break; } else { values.push(value); } } while(true); } //cleanup /*if (valueParts.length){ values.push(new PropertyValue(valueParts, valueParts[0].line, valueParts[0].col)); }*/ return values.length > 0 ? new PropertyValue(values, values[0].line, values[0].col) : null; }, _term: function(){ /* * term * : unary_operator? * [ NUMBER S* | PERCENTAGE S* | LENGTH S* | ANGLE S* | * TIME S* | FREQ S* | function | ie_function ] * | STRING S* | IDENT S* | URI S* | UNICODERANGE S* | hexcolor * ; */ var tokenStream = this._tokenStream, unary = null, value = null, token, line, col; //returns the operator or null unary = this._unary_operator(); if (unary !== null){ line = tokenStream.token().startLine; col = tokenStream.token().startCol; } //exception for IE filters if (tokenStream.peek() == Tokens.IE_FUNCTION && this.options.ieFilters){ value = this._ie_function(); if (unary === null){ line = tokenStream.token().startLine; col = tokenStream.token().startCol; } //see if there's a simple match } else if (tokenStream.match([Tokens.NUMBER, Tokens.PERCENTAGE, Tokens.LENGTH, Tokens.ANGLE, Tokens.TIME, Tokens.FREQ, Tokens.STRING, Tokens.IDENT, Tokens.URI, Tokens.UNICODE_RANGE])){ value = tokenStream.token().value; if (unary === null){ line = tokenStream.token().startLine; col = tokenStream.token().startCol; } this._readWhitespace(); } else { //see if it's a color token = this._hexcolor(); if (token === null){ //if there's no unary, get the start of the next token for line/col info if (unary === null){ line = tokenStream.LT(1).startLine; col = tokenStream.LT(1).startCol; } //has to be a function if (value === null){ /* * This checks for alpha(opacity=0) style of IE * functions. IE_FUNCTION only presents progid: style. */ if (tokenStream.LA(3) == Tokens.EQUALS && this.options.ieFilters){ value = this._ie_function(); } else { value = this._function(); } } /*if (value === null){ return null; //throw new Error("Expected identifier at line " + tokenStream.token().startLine + ", character " + tokenStream.token().startCol + "."); }*/ } else { value = token.value; if (unary === null){ line = token.startLine; col = token.startCol; } } } return value !== null ? new PropertyValuePart(unary !== null ? unary + value : value, line, col) : null; }, _function: function(){ /* * function * : FUNCTION S* expr ')' S* * ; */ var tokenStream = this._tokenStream, functionText = null, expr = null, lt; if (tokenStream.match(Tokens.FUNCTION)){ functionText = tokenStream.token().value; this._readWhitespace(); expr = this._expr(true); functionText += expr; //START: Horrible hack in case it's an IE filter if (this.options.ieFilters && tokenStream.peek() == Tokens.EQUALS){ do { if (this._readWhitespace()){ functionText += tokenStream.token().value; } //might be second time in the loop if (tokenStream.LA(0) == Tokens.COMMA){ functionText += tokenStream.token().value; } tokenStream.match(Tokens.IDENT); functionText += tokenStream.token().value; tokenStream.match(Tokens.EQUALS); functionText += tokenStream.token().value; //functionText += this._term(); lt = tokenStream.peek(); while(lt != Tokens.COMMA && lt != Tokens.S && lt != Tokens.RPAREN){ tokenStream.get(); functionText += tokenStream.token().value; lt = tokenStream.peek(); } } while(tokenStream.match([Tokens.COMMA, Tokens.S])); } //END: Horrible Hack tokenStream.match(Tokens.RPAREN); functionText += ")"; this._readWhitespace(); } return functionText; }, _ie_function: function(){ /* (My own extension) * ie_function * : IE_FUNCTION S* IDENT '=' term [S* ','? IDENT '=' term]+ ')' S* * ; */ var tokenStream = this._tokenStream, functionText = null, expr = null, lt; //IE function can begin like a regular function, too if (tokenStream.match([Tokens.IE_FUNCTION, Tokens.FUNCTION])){ functionText = tokenStream.token().value; do { if (this._readWhitespace()){ functionText += tokenStream.token().value; } //might be second time in the loop if (tokenStream.LA(0) == Tokens.COMMA){ functionText += tokenStream.token().value; } tokenStream.match(Tokens.IDENT); functionText += tokenStream.token().value; tokenStream.match(Tokens.EQUALS); functionText += tokenStream.token().value; //functionText += this._term(); lt = tokenStream.peek(); while(lt != Tokens.COMMA && lt != Tokens.S && lt != Tokens.RPAREN){ tokenStream.get(); functionText += tokenStream.token().value; lt = tokenStream.peek(); } } while(tokenStream.match([Tokens.COMMA, Tokens.S])); tokenStream.match(Tokens.RPAREN); functionText += ")"; this._readWhitespace(); } return functionText; }, _hexcolor: function(){ /* * There is a constraint on the color that it must * have either 3 or 6 hex-digits (i.e., [0-9a-fA-F]) * after the "#"; e.g., "#000" is OK, but "#abcd" is not. * * hexcolor * : HASH S* * ; */ var tokenStream = this._tokenStream, token = null, color; if(tokenStream.match(Tokens.HASH)){ //need to do some validation here token = tokenStream.token(); color = token.value; if (!/#[a-f0-9]{3,6}/i.test(color)){ throw new SyntaxError("Expected a hex color but found '" + color + "' at line " + token.startLine + ", col " + token.startCol + ".", token.startLine, token.startCol); } this._readWhitespace(); } return token; }, //----------------------------------------------------------------- // Animations methods //----------------------------------------------------------------- _keyframes: function(){ /* * keyframes: * : KEYFRAMES_SYM S* keyframe_name S* '{' S* keyframe_rule* '}' { * ; */ var tokenStream = this._tokenStream, token, tt, name, prefix = ""; tokenStream.mustMatch(Tokens.KEYFRAMES_SYM); token = tokenStream.token(); if (/^@\-([^\-]+)\-/.test(token.value)) { prefix = RegExp.$1; } this._readWhitespace(); name = this._keyframe_name(); this._readWhitespace(); tokenStream.mustMatch(Tokens.LBRACE); this.fire({ type: "startkeyframes", name: name, prefix: prefix, line: token.startLine, col: token.startCol }); this._readWhitespace(); tt = tokenStream.peek(); //check for key while(tt == Tokens.IDENT || tt == Tokens.PERCENTAGE) { this._keyframe_rule(); this._readWhitespace(); tt = tokenStream.peek(); } this.fire({ type: "endkeyframes", name: name, prefix: prefix, line: token.startLine, col: token.startCol }); this._readWhitespace(); tokenStream.mustMatch(Tokens.RBRACE); }, _keyframe_name: function(){ /* * keyframe_name: * : IDENT * | STRING * ; */ var tokenStream = this._tokenStream, token; tokenStream.mustMatch([Tokens.IDENT, Tokens.STRING]); return SyntaxUnit.fromToken(tokenStream.token()); }, _keyframe_rule: function(){ /* * keyframe_rule: * : key_list S* * '{' S* declaration [ ';' S* declaration ]* '}' S* * ; */ var tokenStream = this._tokenStream, token, keyList = this._key_list(); this.fire({ type: "startkeyframerule", keys: keyList, line: keyList[0].line, col: keyList[0].col }); this._readDeclarations(true); this.fire({ type: "endkeyframerule", keys: keyList, line: keyList[0].line, col: keyList[0].col }); }, _key_list: function(){ /* * key_list: * : key [ S* ',' S* key]* * ; */ var tokenStream = this._tokenStream, token, key, keyList = []; //must be least one key keyList.push(this._key()); this._readWhitespace(); while(tokenStream.match(Tokens.COMMA)){ this._readWhitespace(); keyList.push(this._key()); this._readWhitespace(); } return keyList; }, _key: function(){ /* * There is a restriction that IDENT can be only "from" or "to". * * key * : PERCENTAGE * | IDENT * ; */ var tokenStream = this._tokenStream, token; if (tokenStream.match(Tokens.PERCENTAGE)){ return SyntaxUnit.fromToken(tokenStream.token()); } else if (tokenStream.match(Tokens.IDENT)){ token = tokenStream.token(); if (/from|to/i.test(token.value)){ return SyntaxUnit.fromToken(token); } tokenStream.unget(); } //if it gets here, there wasn't a valid token, so time to explode this._unexpectedToken(tokenStream.LT(1)); }, //----------------------------------------------------------------- // Helper methods //----------------------------------------------------------------- /** * Not part of CSS grammar, but useful for skipping over * combination of white space and HTML-style comments. * @return {void} * @method _skipCruft * @private */ _skipCruft: function(){ while(this._tokenStream.match([Tokens.S, Tokens.CDO, Tokens.CDC])){ //noop } }, /** * Not part of CSS grammar, but this pattern occurs frequently * in the official CSS grammar. Split out here to eliminate * duplicate code. * @param {Boolean} checkStart Indicates if the rule should check * for the left brace at the beginning. * @param {Boolean} readMargins Indicates if the rule should check * for margin patterns. * @return {void} * @method _readDeclarations * @private */ _readDeclarations: function(checkStart, readMargins){ /* * Reads the pattern * S* '{' S* declaration [ ';' S* declaration ]* '}' S* * or * S* '{' S* [ declaration | margin ]? [ ';' S* [ declaration | margin ]? ]* '}' S* * Note that this is how it is described in CSS3 Paged Media, but is actually incorrect. * A semicolon is only necessary following a delcaration is there's another declaration * or margin afterwards. */ var tokenStream = this._tokenStream, tt; this._readWhitespace(); if (checkStart){ tokenStream.mustMatch(Tokens.LBRACE); } this._readWhitespace(); try { while(true){ if (tokenStream.match(Tokens.SEMICOLON) || (readMargins && this._margin())){ //noop } else if (this._declaration()){ if (!tokenStream.match(Tokens.SEMICOLON)){ break; } } else { break; } //if ((!this._margin() && !this._declaration()) || !tokenStream.match(Tokens.SEMICOLON)){ // break; //} this._readWhitespace(); } tokenStream.mustMatch(Tokens.RBRACE); this._readWhitespace(); } catch (ex) { if (ex instanceof SyntaxError && !this.options.strict){ //fire error event this.fire({ type: "error", error: ex, message: ex.message, line: ex.line, col: ex.col }); //see if there's another declaration tt = tokenStream.advance([Tokens.SEMICOLON, Tokens.RBRACE]); if (tt == Tokens.SEMICOLON){ //if there's a semicolon, then there might be another declaration this._readDeclarations(false, readMargins); } else if (tt != Tokens.RBRACE){ //if there's a right brace, the rule is finished so don't do anything //otherwise, rethrow the error because it wasn't handled properly throw ex; } } else { //not a syntax error, rethrow it throw ex; } } }, /** * In some cases, you can end up with two white space tokens in a * row. Instead of making a change in every function that looks for * white space, this function is used to match as much white space * as necessary. * @method _readWhitespace * @return {String} The white space if found, empty string if not. * @private */ _readWhitespace: function(){ var tokenStream = this._tokenStream, ws = ""; while(tokenStream.match(Tokens.S)){ ws += tokenStream.token().value; } return ws; }, /** * Throws an error when an unexpected token is found. * @param {Object} token The token that was found. * @method _unexpectedToken * @return {void} * @private */ _unexpectedToken: function(token){ throw new SyntaxError("Unexpected token '" + token.value + "' at line " + token.startLine + ", col " + token.startCol + ".", token.startLine, token.startCol); }, /** * Helper method used for parsing subparts of a style sheet. * @return {void} * @method _verifyEnd * @private */ _verifyEnd: function(){ if (this._tokenStream.LA(1) != Tokens.EOF){ this._unexpectedToken(this._tokenStream.LT(1)); } }, //----------------------------------------------------------------- // Validation methods //----------------------------------------------------------------- _validateProperty: function(property, value){ Validation.validate(property, value); }, //----------------------------------------------------------------- // Parsing methods //----------------------------------------------------------------- parse: function(input){ this._tokenStream = new TokenStream(input, Tokens); this._stylesheet(); }, parseStyleSheet: function(input){ //just passthrough return this.parse(input); }, parseMediaQuery: function(input){ this._tokenStream = new TokenStream(input, Tokens); var result = this._media_query(); //if there's anything more, then it's an invalid selector this._verifyEnd(); //otherwise return result return result; }, /** * Parses a property value (everything after the semicolon). * @return {parserlib.css.PropertyValue} The property value. * @throws parserlib.util.SyntaxError If an unexpected token is found. * @method parserPropertyValue */ parsePropertyValue: function(input){ this._tokenStream = new TokenStream(input, Tokens); this._readWhitespace(); var result = this._expr(); //okay to have a trailing white space this._readWhitespace(); //if there's anything more, then it's an invalid selector this._verifyEnd(); //otherwise return result return result; }, /** * Parses a complete CSS rule, including selectors and * properties. * @param {String} input The text to parser. * @return {Boolean} True if the parse completed successfully, false if not. * @method parseRule */ parseRule: function(input){ this._tokenStream = new TokenStream(input, Tokens); //skip any leading white space this._readWhitespace(); var result = this._ruleset(); //skip any trailing white space this._readWhitespace(); //if there's anything more, then it's an invalid selector this._verifyEnd(); //otherwise return result return result; }, /** * Parses a single CSS selector (no comma) * @param {String} input The text to parse as a CSS selector. * @return {Selector} An object representing the selector. * @throws parserlib.util.SyntaxError If an unexpected token is found. * @method parseSelector */ parseSelector: function(input){ this._tokenStream = new TokenStream(input, Tokens); //skip any leading white space this._readWhitespace(); var result = this._selector(); //skip any trailing white space this._readWhitespace(); //if there's anything more, then it's an invalid selector this._verifyEnd(); //otherwise return result return result; }, /** * Parses an HTML style attribute: a set of CSS declarations * separated by semicolons. * @param {String} input The text to parse as a style attribute * @return {void} * @method parseStyleAttribute */ parseStyleAttribute: function(input){ input += "}"; // for error recovery in _readDeclarations() this._tokenStream = new TokenStream(input, Tokens); this._readDeclarations(); } }; //copy over onto prototype for (prop in additions){ if (additions.hasOwnProperty(prop)){ proto[prop] = additions[prop]; } } return proto; }(); /* nth : S* [ ['-'|'+']? INTEGER? {N} [ S* ['-'|'+'] S* INTEGER ]? | ['-'|'+']? INTEGER | {O}{D}{D} | {E}{V}{E}{N} ] S* ; */ /*global Validation, ValidationTypes, ValidationError*/ var Properties = { //A "alignment-adjust" : "auto | baseline | before-edge | text-before-edge | middle | central | after-edge | text-after-edge | ideographic | alphabetic | hanging | mathematical | | ", "alignment-baseline" : "baseline | use-script | before-edge | text-before-edge | after-edge | text-after-edge | central | middle | ideographic | alphabetic | hanging | mathematical", "animation" : 1, "animation-delay" : { multi: "