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):
nameSublimeLinter Annotationsscopesublimelinter.annotationssettingsbackground#FFFFAAforeground#FFFFFFnameSublimeLinter Error Outlinescopesublimelinter.outline.illegalsettingsbackground#FF4A52foreground#FFFFFFnameSublimeLinter Error Underlinescopesublimelinter.underline.illegalsettingsbackground#FF0000nameSublimeLinter Warning Outlinescopesublimelinter.outline.warningsettingsbackground#DF9400foreground#FFFFFFnameSublimeLinter Warning Underlinescopesublimelinter.underline.warningsettingsbackground#FF0000nameSublimeLinter Violation Outlinescopesublimelinter.outline.violationsettingsbackground#ffffff33foreground#FFFFFFnameSublimeLinter Violation Underlinescopesublimelinter.underline.violationsettingsbackground#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 += '
\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: "