├── .gitignore ├── .travis.yml ├── CHANGELOG.md ├── Example SCSScheme.hidden-tmTheme ├── Example SCSScheme.scsscheme ├── Example StyluScheme.hidden-tmTheme ├── Example StyluScheme.styluscheme ├── LICENSE ├── Package ├── CSScheme.YAML-tmLanguage ├── CSScheme.sublime-build ├── CSScheme.sublime-settings ├── CSScheme.tmLanguage ├── Convert to CSScheme.sublime-build ├── Default.sublime-commands ├── Main.sublime-menu ├── SASScheme.YAML-tmLanguage ├── SASScheme.tmLanguage ├── SCSScheme.YAML-tmLanguage ├── SCSScheme.sublime-settings ├── SCSScheme.tmLanguage ├── Snippets │ ├── = to @mixin.sane-snippet │ ├── = to @mixin.sane.sublime-snippet │ ├── @each.sane-snippet │ ├── @each.sane.sublime-snippet │ ├── @else if.sane-snippet │ ├── @else if.sane.sublime-snippet │ ├── @else.sane-snippet │ ├── @else.sane.sublime-snippet │ ├── @for ... through.sane-snippet │ ├── @for ... through.sane.sublime-snippet │ ├── @for ... to.sane-snippet │ ├── @for ... to.sane.sublime-snippet │ ├── @if.sane-snippet │ ├── @if.sane.sublime-snippet │ ├── @mixin.sane-snippet │ ├── @mixin.sane.sublime-snippet │ ├── @while.sane-snippet │ ├── @while.sane.sublime-snippet │ ├── Asterisk Ruleset.sane-snippet │ ├── Asterisk Ruleset.sane.sublime-snippet │ ├── Ruleset.sane-snippet │ └── Ruleset.sane.sublime-snippet ├── StyluScheme.YAML-tmLanguage ├── StyluScheme.tmLanguage └── tmPreferences │ ├── Comments.tmPreferences │ ├── Indentation rules.tmPreferences │ ├── SCSS Comments.tmPreferences │ ├── Symbol List for SCSS at-rules.tmPreferences │ └── Symbol List for selectors.tmPreferences ├── README.md ├── completions.py ├── convert.py ├── converters ├── __init__.py └── tmtheme.py ├── create_new_csscheme.py ├── dev-requirements.txt ├── messages.json ├── messages ├── 0.2.0.md ├── 0.2.1.md ├── 0.3.0.md ├── 1.0.0.md ├── 1.1.0.md ├── 1.1.1.md ├── 1.2.0.md ├── 1.3.0.md └── install.md ├── my_sublime_lib ├── LICENSE.txt ├── __init__.py ├── constants.py ├── edit.py ├── path.py └── view │ ├── __init__.py │ ├── _view.py │ └── output_panel.py ├── scope_data └── __init__.py ├── setup.cfg └── tinycsscheme ├── .coveragerc ├── __init__.py ├── _ordereddict.py ├── css_colors.py ├── dumper.py ├── parser.py ├── test_coverage.bat ├── tests ├── __init__.py ├── css_test.csscheme ├── scss_test.scsscheme ├── test_dumper.py └── test_parser.py └── tinycss ├── LICENSE ├── __init__.py ├── color3.py ├── css21.py ├── decoding.py ├── page3.py ├── parsing.py ├── tests ├── __init__.py ├── speed.py ├── test_api.py ├── test_color3.py ├── test_css21.py ├── test_decoding.py ├── test_page3.py └── test_tokenizer.py ├── token_data.py ├── tokenizer.py └── version.py /.gitignore: -------------------------------------------------------------------------------- 1 | ; Binary files 2 | *.pyc 3 | *.cache 4 | *.coverage 5 | *.dmp 6 | 7 | ; Other irrelevant files and folders 8 | *.sublime-project 9 | *.sublime-workspace 10 | *__pycache__/ 11 | *.sass-cache/ 12 | *htmlcov/ 13 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: python 2 | 3 | # Run on container-based infrastructure 4 | sudo: false 5 | 6 | python: 7 | - "3.3" # ST3 8 | 9 | env: 10 | # Do not do speed tests since they won't work and would generate a failure 11 | - TINYCSS_SKIP_SPEEDUPS_TESTS 12 | 13 | install: 14 | - pip install -r dev-requirements.txt 15 | - pip install coveralls 16 | 17 | script: 18 | - flake8 -v . 19 | # Run tinycss and tinycsscheme tests 20 | - >- 21 | py.test tinycsscheme/tests/ 22 | --cov tinycsscheme 23 | --cov-config tinycsscheme/.coveragerc 24 | --cov-report term-missing 25 | 26 | after_success: 27 | - coveralls 28 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | CSScheme Changelog 2 | ================== 3 | 4 | v1.3.0 (2016-06-23) 5 | ------------------- 6 | 7 | - Prevent `sass` executable from building caches. They were put in weird places 8 | and generally annoying. 9 | - Syntax highlighting changes to CSScheme and SCSScheme 10 | * Multiple scopes have been changed to follow (yet-to-be-specified) 11 | conventions 12 | * Highlighting of all scope selector operators has been added 13 | * Other minor tweaks 14 | - Allow backslash-escaping of any character, specifically for SASS 15 | compatibility with selector operators and scope-segments starting with 16 | numbers (#11) 17 | - Support for the old `'-'` escape sequence has been removed 18 | - `.hidden-tmTheme` files can now also be converted to `.csscheme` 19 | - Added a build system for tmTheme-to-CSScheme conversion 20 | 21 | 22 | v1.2.0 (2015-08-28) 23 | ------------------- 24 | 25 | - You can create `.hidden-tmTheme` files by adding a global `@hidden: true;` 26 | rule to the source. The rule is consumed and the output file's extension 27 | adjusted. (#9) 28 | - The global `@name` rule is now optional. Sublime Text doesn't use it anyway. 29 | - The built example schemes are now hidden, so they don't pop up in the 30 | "Prefereces > Color Scheme" menu anymore 31 | 32 | 33 | v1.1.1 (2015-03-17) 34 | ------------------- 35 | 36 | - 'shadowWidth' is now a known property (as integer) and its value is checked 37 | - Literal integers are now supported, such as `shadowWidth: 10;` 38 | - Completions have received an additional tab trigger to skip the semi colon 39 | 40 | 41 | v1.1.0 (2015-02-14) 42 | ------------------- 43 | 44 | - ST2 support has been removed! Old releases are still available but 45 | development will continue only for ST3. 46 | - Added command to convert from tmTheme to CSScheme ("CSScheme: Convert to 47 | CSScheme") (#8) 48 | - Changed hyphen escape sequence for SASS/SCSS from `'-'` to `\-`, which works 49 | with the current SASS parser (#7) 50 | - Fixed a bug where uuids with leading zeros were not recognized 51 | 52 | 53 | v1.0.0 (2014-08-28) 54 | ------------------- 55 | 56 | - The settings management for executable paths has been changed! 57 | If you depend on this, you'll have to revisit. 58 | - Added support for stylus! (an example file has been bundled as well) 59 | 60 | - If running a pre-compiler, the compiled result will always be shown if there 61 | was an error parsing it 62 | - Added commands to create a new csscheme file (or variation) based on templates 63 | - Added command palette entries to open the readme and settings files 64 | - DumpErrors now show the same debug output as ParseErrors 65 | - Fixed long relative path references in some situations (mainly stylus) 66 | - Fixed wrong syntax file reference with `"preview_compiled_css": true` 67 | - SASScheme files now also get a dedicated syntax which allows CSScheme to more 68 | accurately match its build system (same for stylus). This relies on the 69 | external "Sass" package. 70 | - Fixed wrong line number being displayed when an at-rule was encountered 71 | multiple times 72 | - Added punctuation scopes to auto completion (csscheme, scsscheme) 73 | 74 | 75 | v0.3.0 (2014-03-08) 76 | ------------------- 77 | 78 | - Differentiate between style and options list ("fontStyle" vs e.g. 79 | "tagsOptions") for validation (also #2) 80 | - Allow `"fontStyle": none;` for empty style list (#4) 81 | - Highlight SASS's `index` function 82 | - Fix not showing error message if a line number was not found from the 83 | compiled SCSS (within the last x lines) 84 | - Added snippets for `@for`, `@each`, `@else if`, `@else`, `@while` 85 | - All "package" related files were moved to a sub-directory 86 | 87 | 88 | v0.2.1 (2014-03-01) 89 | ------------------- 90 | 91 | - Added "foreground" to allowed style list properties (e.g. "bracketsOptions") 92 | 93 | 94 | v0.2.0 (2014-02-24) 95 | ------------------- 96 | 97 | - Added more known_properties to check values against (#2) 98 | - Fixed errors when using functions in "unknown" properties (#1) 99 | - Fixed incorrect error messages for empty output from running `sass` 100 | - Fixed unexpected behavior from running `sass` on non-Windows 101 | 102 | 103 | v0.1.1 (2014-02-24) 104 | ------------------- 105 | 106 | - Removed `$` from the SCSS word separator list 107 | 108 | 109 | v0.1.0 (2014-02-22) 110 | ------------------- 111 | 112 | - Initial release 113 | -------------------------------------------------------------------------------- /Example SCSScheme.hidden-tmTheme: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | purpose 6 | This is just an example file to show how you can use this tool 7 | settings 8 | 9 | 10 | settings 11 | 12 | background 13 | #202020 14 | bracketContentsForeground 15 | #69ffb4 16 | bracketContentsOptions 17 | underline 18 | bracketsForeground 19 | #69ffb4 20 | bracketsOptions 21 | underline 22 | caret 23 | #69ffb4 24 | foreground 25 | #FFFFFF 26 | lineHighlight 27 | #FF686833 28 | selection 29 | #FF68684C 30 | tagsForeground 31 | #69ffb4 32 | tagsOptions 33 | underline 34 | 35 | 36 | 37 | name 38 | String 39 | scope 40 | string 41 | settings 42 | 43 | foreground 44 | #FF6868 45 | 46 | 47 | 48 | scope 49 | string punctuation 50 | settings 51 | 52 | foreground 53 | #68ffff 54 | 55 | 56 | 57 | scope 58 | string.constant 59 | settings 60 | 61 | foreground 62 | #ff6868 63 | 64 | 65 | 66 | scope 67 | constant 68 | settings 69 | 70 | foreground 71 | #e8b37f 72 | 73 | 74 | 75 | scope 76 | constant.numeric 77 | settings 78 | 79 | foreground 80 | #FFB368 81 | 82 | 83 | 84 | scope 85 | comment 86 | settings 87 | 88 | fontStyle 89 | italic 90 | foreground 91 | #FFFE68 92 | 93 | 94 | 95 | scope 96 | comment punctuation 97 | settings 98 | 99 | foreground 100 | #9b9a00 101 | 102 | 103 | 104 | scope 105 | support 106 | settings 107 | 108 | foreground 109 | #B4FF68 110 | 111 | 112 | 113 | scope 114 | support.constant 115 | settings 116 | 117 | foreground 118 | #f0b377 119 | 120 | 121 | 122 | scope 123 | entity 124 | settings 125 | 126 | foreground 127 | #69FF68 128 | 129 | 130 | 131 | scope 132 | entity.name - entity.name.tag 133 | settings 134 | 135 | background 136 | #7FE87F40 137 | 138 | 139 | 140 | scope 141 | invalid 142 | settings 143 | 144 | foreground 145 | #68FFFE 146 | 147 | 148 | 149 | scope 150 | invalid.illegal 151 | settings 152 | 153 | background 154 | #FF686966 155 | foreground 156 | #68FFFE 157 | 158 | 159 | 160 | scope 161 | keyword 162 | settings 163 | 164 | foreground 165 | #68B4FF 166 | 167 | 168 | 169 | scope 170 | storage 171 | settings 172 | 173 | foreground 174 | #6869FF 175 | 176 | 177 | 178 | scope 179 | variable, support.variable 180 | settings 181 | 182 | foreground 183 | #B368FF 184 | 185 | 186 | 187 | scope 188 | (source.csscheme, source.scsscheme) & (meta.selector - punctuation.section.group), markup.heading.1 189 | settings 190 | 191 | fontStyle 192 | italic 193 | 194 | 195 | 196 | 197 | 198 | -------------------------------------------------------------------------------- /Example SCSScheme.scsscheme: -------------------------------------------------------------------------------- 1 | @purpose "This is just an example file to show how you can use this tool"; 2 | 3 | // This at-rule is optional 4 | // @name "Example SCSScheme"; 5 | 6 | // This at-rule tells CSScheme to generate a `.hidden-tmTheme` file 7 | @hidden true; 8 | 9 | // FLAGS 10 | $punctuation: true; 11 | 12 | // The color palette (from http://serennu.com/colour/colourcalculator.php) 13 | // Don't try these at home! 14 | $back: #202020; 15 | $fore: #FFF; 16 | 17 | $col0: #FF69B4; // Hotpink Colour Wheel 18 | $col1: #FF6868; // (adjacent) 19 | $col2: #FFB368; 20 | $col3: #FFFE68; 21 | $col4: #B4FF68; // (triad) 22 | $col5: #69FF68; // (split complementary) 23 | $col6: #68FFB3; // (complementary) 24 | $col7: #68FFFE; // (split complementary) 25 | $col8: #68B4FF; // (triad) 26 | $col9: #6869FF; 27 | $col10: #B368FF; 28 | $col11: #FE68FF; // (adjacent) 29 | 30 | $caret: complement($col0); // same as $col6, probably 31 | 32 | // This '*' rule is required too, it will serve as the general-purpose settings 33 | // such as the global background color and line highlight background color. 34 | * { 35 | background: $back; 36 | foreground: $fore; 37 | 38 | caret: $caret; 39 | lineHighlight: transparentize($col1, 0.8); 40 | selection: transparentize($col1, 0.7); 41 | 42 | @each $pre in bracketContents, brackets, tags { 43 | #{$pre}Foreground: $caret; 44 | #{$pre}Options: underline; 45 | } 46 | } 47 | 48 | @mixin contrast($col) { 49 | foreground: $col; 50 | background: transparentize(complement($col), .6); // or invert() 51 | } 52 | 53 | string { 54 | /* This is the name that will be displayed when editing the file in a different 55 | * color scheme editor - after compilation. Usually you won't need this. */ 56 | @name "String"; 57 | 58 | foreground: $col1; 59 | 60 | @if $punctuation { 61 | punctuation { 62 | foreground: complement($col1); 63 | } 64 | } 65 | 66 | &.constant { 67 | foreground: saturate($col1, 20%); 68 | } 69 | } 70 | 71 | constant { 72 | foreground: desaturate($col2, 30%); 73 | 74 | &.numeric { 75 | foreground: $col2; 76 | } 77 | } 78 | 79 | comment { 80 | foreground: $col3; 81 | fontStyle: italic; 82 | 83 | @if $punctuation { 84 | punctuation { 85 | foreground: darken($col3, 40%); 86 | } 87 | } 88 | } 89 | 90 | support { 91 | foreground: $col4; 92 | 93 | &.constant { 94 | foreground: desaturate($col2, 20%); 95 | } 96 | } 97 | 98 | entity { 99 | foreground: $col5; 100 | 101 | // We need to escape the subtract operator here because SASS doesn't 102 | // recognize it as a valid selector otherwise. 103 | // The dumper will take care of it. 104 | &.name \- &.name.tag { 105 | background: transparentize(desaturate($col5, 30%), .75); 106 | } 107 | } 108 | 109 | invalid { 110 | foreground: $col7; 111 | 112 | &.illegal { 113 | @include contrast($col7); // alt. $back 114 | } 115 | } 116 | 117 | keyword { 118 | foreground: $col8; 119 | } 120 | 121 | storage { 122 | foreground: $col9; 123 | } 124 | 125 | variable, support.variable { 126 | foreground: $col10; 127 | } 128 | 129 | // This is an example of advanced operator usage ... 130 | \(source.csscheme, source.scsscheme\) \& \(meta.selector \- punctuation.section.group\), 131 | // ... and using backslashes to escape any character. 132 | markup.heading.\1 { 133 | fontStyle: italic; 134 | } 135 | -------------------------------------------------------------------------------- /Example StyluScheme.hidden-tmTheme: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | name 6 | Example StyluScheme 7 | settings 8 | 9 | 10 | settings 11 | 12 | background 13 | #202020 14 | bracketContentsForeground 15 | #69ffb4 16 | bracketContentsOptions 17 | underline 18 | bracketsForeground 19 | #69ffb4 20 | bracketsOptions 21 | underline 22 | caret 23 | #69ffb4 24 | foreground 25 | #ffffff 26 | lineHighlight 27 | #FF686833 28 | selection 29 | #FF68684C 30 | tagsForeground 31 | #69ffb4 32 | tagsOptions 33 | underline 34 | 35 | 36 | 37 | scope 38 | string 39 | settings 40 | 41 | foreground 42 | #ff6868 43 | 44 | 45 | 46 | scope 47 | string punctuation 48 | settings 49 | 50 | foreground 51 | #68ffff 52 | 53 | 54 | 55 | scope 56 | string.constant 57 | settings 58 | 59 | foreground 60 | #ff5959 61 | 62 | 63 | 64 | scope 65 | constant 66 | settings 67 | 68 | foreground 69 | #e8b37f 70 | 71 | 72 | 73 | scope 74 | constant.numeric 75 | settings 76 | 77 | foreground 78 | #ffb368 79 | 80 | 81 | 82 | scope 83 | comment 84 | settings 85 | 86 | fontStyle 87 | italic 88 | foreground 89 | #fffe68 90 | 91 | 92 | 93 | scope 94 | comment punctuation 95 | settings 96 | 97 | foreground 98 | #d7d600 99 | 100 | 101 | 102 | scope 103 | support 104 | settings 105 | 106 | foreground 107 | #b4ff68 108 | 109 | 110 | 111 | scope 112 | support.constant 113 | settings 114 | 115 | foreground 116 | #f0b377 117 | 118 | 119 | 120 | scope 121 | entity 122 | settings 123 | 124 | foreground 125 | #69ff68 126 | 127 | 128 | 129 | scope 130 | entity.name - entity.name.tag 131 | settings 132 | 133 | background 134 | #7FE87F40 135 | 136 | 137 | 138 | scope 139 | invalid 140 | settings 141 | 142 | foreground 143 | #68fffe 144 | 145 | 146 | 147 | scope 148 | invalid.illegal 149 | settings 150 | 151 | background 152 | #ff6869 153 | 154 | 155 | 156 | scope 157 | keyword 158 | settings 159 | 160 | foreground 161 | #68b4ff 162 | 163 | 164 | 165 | scope 166 | storage 167 | settings 168 | 169 | foreground 170 | #6869ff 171 | 172 | 173 | 174 | scope 175 | variable, support.variable 176 | settings 177 | 178 | foreground 179 | #b368ff 180 | 181 | 182 | 183 | 184 | 185 | -------------------------------------------------------------------------------- /Example StyluScheme.styluscheme: -------------------------------------------------------------------------------- 1 | // This at-rule is optional, and needs the unquote function 2 | unquote('@name "Example StyluScheme";') 3 | 4 | // This at-rule tells CSScheme to generate a `.hidden-tmTheme` file 5 | unquote('@hidden true;') 6 | 7 | warn("this should be displayed in the output panel on build") 8 | 9 | // FLAGS 10 | $punctuation = true 11 | 12 | // The color palette (from http://serennu.com/colour/colourcalculator.php) 13 | // Don't try these at home! 14 | $back = #202020 15 | $fore = #FFF 16 | 17 | $col0 = #FF69B4 // Hotpink Colour Wheel 18 | $col1 = #FF6868 // (adjacent) 19 | $col2 = #FFB368 20 | $col3 = #FFFE68 21 | $col4 = #B4FF68 // (triad) 22 | $col5 = #69FF68 // (split complementary) 23 | $col6 = #68FFB3 // (complementary) 24 | $col7 = #68FFFE // (split complementary) 25 | $col8 = #68B4FF // (triad) 26 | $col9 = #6869FF 27 | $col10 = #B368FF 28 | $col11 = #FE68FF // (adjacent) 29 | 30 | $caret = complement($col0) // same as $col6, probably 31 | 32 | // This '*' rule is required too, it will serve as the general-purpose settings 33 | // such as the global background color and line highlight background color. 34 | * 35 | background: $back 36 | foreground: $fore 37 | 38 | caret: $caret 39 | lineHighlight: rgba($col1, .2) 40 | selection: rgba($col1, 0.3) 41 | 42 | for $pre in bracketContents brackets tags 43 | {$pre}Foreground: $caret 44 | {$pre}Options: underline 45 | 46 | 47 | contrast() { 48 | background: complement(@foreground) 49 | } 50 | 51 | 52 | string 53 | /* This *would* be the name to be displayed when editing the file in 54 | * a different color scheme editor - after compilation, but IT DOESN'T WORK 55 | * in stylus. 56 | */ 57 | 58 | //unquote('@name "String";') 59 | 60 | foreground $col1 61 | 62 | if $punctuation 63 | punctuation 64 | foreground complement(@foreground) 65 | 66 | &.constant 67 | foreground saturate($col1, 20%) 68 | 69 | 70 | 71 | constant { 72 | foreground: desaturate($col2, 30%); 73 | 74 | &.numeric { 75 | foreground: $col2; 76 | } 77 | } 78 | 79 | comment { 80 | foreground: $col3; 81 | fontStyle: italic; 82 | 83 | if $punctuation { 84 | punctuation { 85 | foreground: darken($col3, 40%) 86 | } 87 | } 88 | } 89 | 90 | support { 91 | foreground: $col4 92 | 93 | &.constant { 94 | foreground: desaturate($col2, 20%) 95 | } 96 | } 97 | 98 | entity { 99 | foreground: $col5 100 | 101 | &.name - &.name.tag { 102 | background: rgba(desaturate($col5, 30%), .25) 103 | } 104 | } 105 | 106 | invalid { 107 | foreground: $col7 108 | 109 | &.illegal { 110 | contrast() // alt. $back 111 | } 112 | } 113 | 114 | keyword { 115 | foreground: $col8 116 | } 117 | 118 | storage 119 | foreground: $col9 120 | 121 | 122 | variable, support.variable 123 | foreground $col10 124 | 125 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2014 FichteFoll 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all 11 | copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | SOFTWARE. 20 | -------------------------------------------------------------------------------- /Package/CSScheme.YAML-tmLanguage: -------------------------------------------------------------------------------- 1 | # [PackageDev] target_format: plist, ext: tmLanguage 2 | --- 3 | name: CSScheme 4 | scopeName: source.csscheme 5 | fileTypes: [csscheme] 6 | 7 | patterns: 8 | - include: '#comment-block' 9 | - include: '#at-rule' 10 | - include: '#selector' 11 | - include: '#ruleset' 12 | 13 | repository: 14 | comment-block: 15 | name: comment.block.css 16 | begin: /\* 17 | beginCaptures: 18 | '0': {name: punctuation.definition.begin.comment.csscheme} 19 | end: \*/ 20 | endCaptures: 21 | '0': {name: punctuation.definition.end.comment.csscheme} 22 | 23 | at-rule: 24 | name: meta.at-rule.arbitrary.csscheme 25 | begin: ((@)\w[\w_-]*)\b 26 | beginCaptures: 27 | # keyword.control.at-rule.arbitrary.csscheme 28 | '1': {name: entity.name.at-rule.arbitrary.csscheme} 29 | '2': {name: punctuation.definition.begin.at-rule.csscheme} 30 | end: ; 31 | endCaptures: 32 | '0': {name: punctuation.terminator.at-rule.csscheme} 33 | patterns: 34 | - include: '#string' 35 | - include: '#uuid' 36 | - include: '#number' 37 | - include: '#ident' 38 | 39 | selector: 40 | name: meta.selector.csscheme 41 | begin: (?=[*a-zA-Z()\\-]) 42 | end: \s*(?=\{) 43 | patterns: 44 | - include: '#comment-block' 45 | - include: '#selector-patterns' 46 | 47 | selector-patterns: 48 | patterns: 49 | - include: '#selector-operators' 50 | - name: constant.language.wildcard.csscheme # our special "settings" selector 51 | match: \* 52 | - name: meta.scope-token.csscheme 53 | match: '[\w_.-]+' # technically, more characters are supported but nowhere used 54 | - name: constant.character.escape.csscheme 55 | match: '\\.' 56 | - name: invalid.illegal.selector.csscheme 57 | match: '.' 58 | 59 | selector-operators: 60 | # all backslash-escape variants are for the SASS pre-processor 61 | patterns: 62 | - name: keyword.operator.subtraction.csscheme 63 | match: -|\\- 64 | - name: keyword.operator.intersection.csscheme 65 | match: '&|\\&' 66 | - name: keyword.operator.union.csscheme 67 | match: ',|\||\\\|' 68 | - name: keyword.operator.nesting.csscheme 69 | match: \s 70 | - begin: \(|\\\( 71 | end: \)|\\\) 72 | captures: 73 | '0': {name: keyword.operator.group.csscheme} 74 | name: meta.group.csscheme 75 | patterns: 76 | - include: '#selector-patterns' 77 | 78 | ruleset: 79 | name: meta.ruleset.csscheme 80 | begin: \{ 81 | beginCaptures: 82 | '0': {name: punctuation.definition.begin.ruleset.csscheme} 83 | end: \} 84 | endCaptures: 85 | '0': {name: punctuation.definition.end.ruleset.csscheme} 86 | patterns: 87 | - include: '#comment-block' 88 | - include: '#at-rule' 89 | - include: '#properties' 90 | 91 | properties: 92 | name: meta.property.csscheme 93 | begin: | 94 | (?x) 95 | \b(?:(background|foreground|caret|invisibles|lineHighlight|selection| 96 | activeGuide|fontStyle|tagsOptions) 97 | |([a-zA-Z_-]+) 98 | ) 99 | \s* 100 | (:) 101 | beginCaptures: 102 | '1': {name: keyword.other.property.known.csscheme} 103 | '2': {name: support.other.property.arbitrary.csscheme} 104 | '3': {name: punctuation.separator.property-value.csscheme} 105 | end: (;)|(?=\}) 106 | endCaptures: 107 | '1': {name: punctuation.terminator.property.csscheme} 108 | patterns: 109 | - include: '#comment-block' 110 | - include: '#values' 111 | 112 | values: 113 | patterns: 114 | - include: '#number' 115 | - include: '#color' 116 | - include: '#style' 117 | - include: '#string' 118 | 119 | string: 120 | patterns: 121 | - name: string.quoted.double.css 122 | begin: '"' 123 | beginCaptures: 124 | '0': {name: punctuation.definition.string.begin.csscheme} 125 | end: '"' 126 | endCaptures: 127 | '0': {name: punctuation.definition.string.end.csscheme} 128 | patterns: 129 | - name: constant.character.escape.css 130 | match: \\. 131 | 132 | - name: string.quoted.single.css 133 | begin: "'" 134 | beginCaptures: 135 | '0': {name: punctuation.definition.string.begin.csscheme} 136 | end: "'" 137 | endCaptures: 138 | '0': {name: punctuation.definition.string.end.csscheme} 139 | patterns: 140 | - match: \\. 141 | name: constant.character.escape.csscheme 142 | 143 | color: 144 | patterns: 145 | - name: constant.other.color-hash.csscheme 146 | match: (? Package Settings > 7 | // CSScheme > Settings - User" and paste. 8 | // 9 | // Read the update notices for new or changed settings. 10 | //////////////////////////////////////////////////////////////////////////// 11 | 12 | /* Set this to true if you would like to open the .tmTheme file after 13 | * building. 14 | */ 15 | "open_after_build": false, 16 | 17 | /* If true will open a temporary view that contains the results of the 18 | * conversion to css (if performed). Useful for debugging. 19 | */ 20 | "preview_compiled_css": false, 21 | 22 | /* If you plan on using SCSS (or SASS) for conversion to tmTheme you have to 23 | * make sure that "sass" is availale on your PATH or specify the path to 24 | * the executable here. (`shell` is set to true on Windows, so you don't 25 | * have to include the extension.) 26 | */ 27 | "executables": { 28 | "sass": "sass", 29 | "stylus": "stylus" 30 | }, 31 | 32 | /* Makes "." dots trigger the auto completion popup. You usually don't want 33 | * to modify this. 34 | */ 35 | "auto_complete_triggers": 36 | [ 37 | { 38 | "characters": ".", 39 | "selector": "source.csscheme meta.selector" 40 | } 41 | ] 42 | } 43 | -------------------------------------------------------------------------------- /Package/Convert to CSScheme.sublime-build: -------------------------------------------------------------------------------- 1 | { 2 | "target": "convert_tmtheme", 3 | "selector": "text.xml" 4 | } 5 | -------------------------------------------------------------------------------- /Package/Default.sublime-commands: -------------------------------------------------------------------------------- 1 | [ 2 | // Convert (also, and mainly, as build system) 3 | { 4 | "caption": "CSScheme: Convert to tmTheme", 5 | "command": "convert_csscheme" 6 | }, 7 | { 8 | "caption": "CSScheme: Convert to CSScheme", 9 | "command": "convert_tmtheme" 10 | }, 11 | { 12 | "caption": "CSScheme: Convert to CSScheme (skip @names)", 13 | "command": "convert_tmtheme", 14 | "args": {"skip_names": true} 15 | }, 16 | 17 | // Open readme and settings 18 | { 19 | "caption": "CSScheme: Open README", 20 | "command": "open_file", 21 | "args": {"file": "${packages}/CSScheme/README.md"} 22 | }, 23 | { 24 | "caption": "Preferences: CSScheme Settings - Default", 25 | "command": "open_file", 26 | "args": {"file": "${packages}/CSScheme/Package/CSScheme.sublime-settings"} 27 | }, 28 | { 29 | "caption": "Preferences: CSScheme Settings - User", 30 | "command": "open_file", 31 | "args": {"file": "${packages}/User/CSScheme.sublime-settings", "contents": "{\n\t$0\n}"} 32 | }, 33 | 34 | // Commands for new file creation 35 | { 36 | "caption": "CSScheme: Create new CSScheme file", 37 | "command": "create_csscheme", 38 | "args": {"syntax": "CSScheme"} 39 | }, 40 | { 41 | "caption": "CSScheme: Create new SCSScheme file", 42 | "command": "create_csscheme", 43 | "args": {"syntax": "SCSScheme"} 44 | }, 45 | { 46 | "caption": "CSScheme: Create new SASScheme file", 47 | "command": "create_csscheme", 48 | "args": {"syntax": "SASScheme"} 49 | }, 50 | { 51 | "caption": "CSScheme: Create new StyluScheme file", 52 | "command": "create_csscheme", 53 | "args": {"syntax": "StyluScheme"} 54 | } 55 | ] 56 | -------------------------------------------------------------------------------- /Package/Main.sublime-menu: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "id": "preferences", 4 | "children": 5 | [ 6 | { 7 | // Include this information in case it is the only package using that menu 8 | "caption": "Package Settings", 9 | "mnemonic": "P", 10 | "id": "package-settings", 11 | "children": 12 | [ 13 | { 14 | "caption": "CSScheme", 15 | "id": "csscheme", 16 | "children": 17 | [ 18 | // README 19 | { 20 | "caption": "README", 21 | "command": "open_file", 22 | "args": {"file": "${packages}/CSScheme/README.md"} 23 | }, 24 | { "caption": "-"}, 25 | // Settings - Default 26 | { 27 | "caption": "Settings – Default", 28 | "command": "open_file", 29 | "args": {"file": "${packages}/CSScheme/Package/CSScheme.sublime-settings"} 30 | }, 31 | // Settings - User 32 | { 33 | "caption": "Settings – User", 34 | "command": "open_file", 35 | "args": {"file": "${packages}/User/CSScheme.sublime-settings", "contents": "{\n\t$0\n}"} 36 | } 37 | ] 38 | } 39 | ] 40 | } 41 | ] 42 | } 43 | ] 44 | -------------------------------------------------------------------------------- /Package/SASScheme.YAML-tmLanguage: -------------------------------------------------------------------------------- 1 | # [PackageDev] target_format: plist, ext: tmLanguage 2 | --- 3 | name: SASScheme 4 | scopeName: source.sass.sasscheme 5 | fileTypes: [sasscheme] 6 | uuid: 0e8ef586-2e86-48f6-9637-f7d17144d09b 7 | 8 | # We only use this to mask our files with a special scope 9 | patterns: 10 | - include: source.sass 11 | ... 12 | -------------------------------------------------------------------------------- /Package/SASScheme.tmLanguage: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | fileTypes 6 | 7 | sasscheme 8 | 9 | name 10 | SASScheme 11 | patterns 12 | 13 | 14 | include 15 | source.sass 16 | 17 | 18 | scopeName 19 | source.sass.sasscheme 20 | uuid 21 | 0e8ef586-2e86-48f6-9637-f7d17144d09b 22 | 23 | 24 | -------------------------------------------------------------------------------- /Package/SCSScheme.sublime-settings: -------------------------------------------------------------------------------- 1 | { 2 | "auto_complete_triggers": 3 | [ 4 | { 5 | "characters": ".", 6 | "selector": "source.csscheme meta.selector" 7 | } 8 | ], 9 | // Characters that are considered to separate words 10 | // Same as default except $ 11 | "word_separators": "./\\()\"'-:,.;<>~!@#%^&*|+=[]{}`~?" 12 | } -------------------------------------------------------------------------------- /Package/Snippets/= to @mixin.sane-snippet: -------------------------------------------------------------------------------- 1 | --- 2 | description: = Shortcut to @mixin 3 | tabTrigger: = 4 | scope: source.csscheme.scss 5 | --- 6 | @mixin ${1:mixin-name}${2:(${3:\$params})} { 7 | $0 8 | } 9 | -------------------------------------------------------------------------------- /Package/Snippets/= to @mixin.sane.sublime-snippet: -------------------------------------------------------------------------------- 1 | = Shortcut to @mixin=source.csscheme.scss -------------------------------------------------------------------------------- /Package/Snippets/@each.sane-snippet: -------------------------------------------------------------------------------- 1 | --- 2 | description: @each 3 | tabTrigger: each 4 | scope: source.csscheme.scss 5 | --- 6 | @each \$${1:el} in ${2:list} { 7 | $0 8 | } 9 | -------------------------------------------------------------------------------- /Package/Snippets/@each.sane.sublime-snippet: -------------------------------------------------------------------------------- 1 | @eacheachsource.csscheme.scss -------------------------------------------------------------------------------- /Package/Snippets/@else if.sane-snippet: -------------------------------------------------------------------------------- 1 | --- 2 | description: @else if 3 | tabTrigger: elif 4 | scope: source.csscheme.scss 5 | --- 6 | @else if ${1:conditions} { 7 | $0 8 | } 9 | -------------------------------------------------------------------------------- /Package/Snippets/@else if.sane.sublime-snippet: -------------------------------------------------------------------------------- 1 | @else ifelifsource.csscheme.scss -------------------------------------------------------------------------------- /Package/Snippets/@else.sane-snippet: -------------------------------------------------------------------------------- 1 | --- 2 | description: @else 3 | tabTrigger: else 4 | scope: source.csscheme.scss 5 | --- 6 | @else { 7 | $0 8 | } 9 | -------------------------------------------------------------------------------- /Package/Snippets/@else.sane.sublime-snippet: -------------------------------------------------------------------------------- 1 | @elseelsesource.csscheme.scss -------------------------------------------------------------------------------- /Package/Snippets/@for ... through.sane-snippet: -------------------------------------------------------------------------------- 1 | --- 2 | description: @for ... through 3 | tabTrigger: fort 4 | scope: source.csscheme.scss 5 | --- 6 | @for \$${1:i} from ${2:1} through ${3:4} { 7 | $0 8 | } 9 | -------------------------------------------------------------------------------- /Package/Snippets/@for ... through.sane.sublime-snippet: -------------------------------------------------------------------------------- 1 | @for ... throughfortsource.csscheme.scss -------------------------------------------------------------------------------- /Package/Snippets/@for ... to.sane-snippet: -------------------------------------------------------------------------------- 1 | --- 2 | description: @for ... to 3 | tabTrigger: for 4 | scope: source.csscheme.scss 5 | --- 6 | @for \$${1:i} from ${2:1} to ${3:4} { 7 | $0 8 | } 9 | -------------------------------------------------------------------------------- /Package/Snippets/@for ... to.sane.sublime-snippet: -------------------------------------------------------------------------------- 1 | @for ... toforsource.csscheme.scss -------------------------------------------------------------------------------- /Package/Snippets/@if.sane-snippet: -------------------------------------------------------------------------------- 1 | --- 2 | description: @if 3 | tabTrigger: if 4 | scope: source.csscheme.scss 5 | --- 6 | @if ${1:conditions} { 7 | $0 8 | } 9 | -------------------------------------------------------------------------------- /Package/Snippets/@if.sane.sublime-snippet: -------------------------------------------------------------------------------- 1 | @ififsource.csscheme.scss -------------------------------------------------------------------------------- /Package/Snippets/@mixin.sane-snippet: -------------------------------------------------------------------------------- 1 | --- 2 | description: @mixin 3 | tabTrigger: mixin 4 | scope: source.csscheme.scss 5 | --- 6 | @mixin ${1:mixin-name}${2:(${3:\$params})} { 7 | $0 8 | } 9 | -------------------------------------------------------------------------------- /Package/Snippets/@mixin.sane.sublime-snippet: -------------------------------------------------------------------------------- 1 | @mixinmixinsource.csscheme.scss -------------------------------------------------------------------------------- /Package/Snippets/@while.sane-snippet: -------------------------------------------------------------------------------- 1 | --- 2 | description: @while 3 | tabTrigger: while 4 | scope: source.csscheme.scss 5 | --- 6 | @while ${1:\$i > 0} { 7 | $0 8 | } 9 | -------------------------------------------------------------------------------- /Package/Snippets/@while.sane.sublime-snippet: -------------------------------------------------------------------------------- 1 | @whilewhilesource.csscheme.scss 0} { 3 | $0 4 | } 5 | ]]> -------------------------------------------------------------------------------- /Package/Snippets/Asterisk Ruleset.sane-snippet: -------------------------------------------------------------------------------- 1 | --- 2 | description: * Ruleset 3 | tabTrigger: * 4 | scope: source.csscheme 5 | --- 6 | * { 7 | foreground: $1; 8 | background: $2; 9 | 10 | caret: $4; 11 | lineHighlight: $5; 12 | selection: $6;$0 13 | } 14 | -------------------------------------------------------------------------------- /Package/Snippets/Asterisk Ruleset.sane.sublime-snippet: -------------------------------------------------------------------------------- 1 | * Ruleset*source.csscheme -------------------------------------------------------------------------------- /Package/Snippets/Ruleset.sane-snippet: -------------------------------------------------------------------------------- 1 | --- 2 | description: Ruleset 3 | tabTrigger: r 4 | scope: source.csscheme 5 | --- 6 | ${1:source} { 7 | foreground: $2; 8 | background: $3; 9 | fontStyle: $4;$0 10 | } 11 | -------------------------------------------------------------------------------- /Package/Snippets/Ruleset.sane.sublime-snippet: -------------------------------------------------------------------------------- 1 | Rulesetrsource.csscheme -------------------------------------------------------------------------------- /Package/StyluScheme.YAML-tmLanguage: -------------------------------------------------------------------------------- 1 | # [PackageDev] target_format: plist, ext: tmLanguage 2 | --- 3 | name: StyluScheme 4 | scopeName: source.stylus.styluscheme 5 | fileTypes: [styluscheme] 6 | uuid: 0e8ef586-2e86-48f6-9637-f7d17144d09b 7 | 8 | # We only use this to mask our files with a special scope 9 | patterns: 10 | - include: source.stylus 11 | ... 12 | -------------------------------------------------------------------------------- /Package/StyluScheme.tmLanguage: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | fileTypes 6 | 7 | name 8 | StyluScheme 9 | patterns 10 | 11 | 12 | include 13 | source.stylus 14 | 15 | 16 | scopeName 17 | source.stylus.styluscheme 18 | uuid 19 | 0e8ef586-2e86-48f6-9637-f7d17144d09b 20 | 21 | 22 | -------------------------------------------------------------------------------- /Package/tmPreferences/Comments.tmPreferences: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | name 6 | Comments 7 | scope 8 | source.csscheme 9 | settings 10 | 11 | shellVariables 12 | 13 | 14 | name 15 | TM_COMMENT_START 16 | value 17 | /* 18 | 19 | 20 | name 21 | TM_COMMENT_END 22 | value 23 | */ 24 | 25 | 26 | 27 | 28 | 29 | -------------------------------------------------------------------------------- /Package/tmPreferences/Indentation rules.tmPreferences: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | name 6 | Indentation rules 7 | scope 8 | source.csscheme 9 | settings 10 | 11 | increaseIndentPattern 12 | ^.*\{[^}]*?$ 13 | decreaseIndentPattern 14 | ^[^{\n]*?\} 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /Package/tmPreferences/SCSS Comments.tmPreferences: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | name 6 | Comments 7 | scope 8 | source.csscheme.scss 9 | settings 10 | 11 | shellVariables 12 | 13 | 14 | name 15 | TM_COMMENT_START 16 | value 17 | // 18 | 19 | 20 | name 21 | TM_COMMENT_START_2 22 | value 23 | /* 24 | 25 | 26 | name 27 | TM_COMMENT_END_2 28 | value 29 | */ 30 | 31 | 32 | 33 | 34 | 35 | -------------------------------------------------------------------------------- /Package/tmPreferences/Symbol List for SCSS at-rules.tmPreferences: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | scope 6 | meta.at-rule.function.csscheme, meta.at-rule.mixin.csscheme 7 | settings 8 | 9 | showInSymbolList 10 | 1 11 | symbolTransformation 12 | s/\s+\{.*// 13 | showInIndexedSymbolList 14 | 1 15 | symbolIndexTransformation 16 | s/\s+\{.*// 17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /Package/tmPreferences/Symbol List for selectors.tmPreferences: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | scope 6 | meta.selector.csscheme 7 | settings 8 | 9 | showInSymbolList 10 | 1 11 | symbolTransformation 12 | s/^/CSS: / 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | CSScheme - Sublime Text Plugin 2 | ============================== 3 | 4 | [![Build Status][]](https://travis-ci.org/FichteFoll/CSScheme) 5 | 6 | [Build Status]: https://travis-ci.org/FichteFoll/CSScheme.png 7 | 8 | Ever thought handwriting `.tmTheme` files sucks? But the other options for 9 | editing color schemes are not programmatical enough? Then this is for you! 10 | 11 | ![What it looks like](http://i.imgur.com/0LTV2xq.gif) 12 | 13 | CSScheme is a custom CSS-like syntax that converts into the `.tmTheme` files we 14 | all love, but it does not end there. CSScheme (the package) can also take care 15 | of **compiling SCSS, SASS or stylus** into CSScheme (the syntax) and *then* into 16 | a color scheme using all features of these pre-compilers, such as variables, 17 | conditionals or functions. 18 | 19 | *Check the [example files](#example-files) for what's possible!* 20 | 21 | 22 | ## Installation 23 | 24 | Use [Package Control][] to [install][] "CSScheme". 25 | 26 | [Package Control]: https://packagecontrol.io/installation 27 | [install]: https://packagecontrol.io/docs/usage 28 | 29 | 30 | ## Usage (Please Read!) 31 | 32 | You can either create a new file with the **CSScheme: Create new \*Scheme file** 33 | commands, open a file with the `.csscheme`, `.scsscheme`, `.sasscheme` or 34 | `.styluscheme` extension or convert an existing `tmTheme` file using the 35 | **CSScheme: Convert to CSScheme** command or build system. Conversion to other 36 | syntaxes is not supported at the moment and likely won't in the future. Please 37 | convert manually and to your own preferences. 38 | 39 | Building (ctrl+b or ⌘b) will convert the file to CSScheme, 40 | if necessary, and then into a `.tmTheme` file. Errors during conversion are 41 | captured in an output panel. For automation purposes, the command is named 42 | `convert_csscheme`. 43 | 44 | Things you *must* consider when using **CSScheme**: 45 | 46 | - `@` at-rules will be added as string values to the "outer dictionary". You 47 | *may* specify a global `@name` rule to specify the scheme's name. `@name` 48 | rules in a ruleset will show as the name for various color scheme editing 49 | tools after compilation. Sublime Text itself does not use it. 50 | - In order to create a `.hidden-tmTheme` file, you need to specify a global 51 | `@hidden true;` rule. The converter will consume this rule and change the 52 | output file's extension accordingly. 53 | - If you want a property to have no font styles you have to specify 54 | `fontStyle: none;`. This will be translated to 55 | `fontStyle`. 56 | - The general settings (like main background color) are read from a general- 57 | purpose block with a `*` selector. This is required. 58 | - Specifying a uuid (via `@uuid`) is optional because Sublime Text ignores it. 59 | 60 | 61 | Things you *must* consider additionally when using CSScheme with **SCSS** or 62 | **SASS**: 63 | 64 | - Make sure that `sass` is available on your PATH or adjust the path to the 65 | executable in the settings. 66 | - The SASS parser will not accept raw `#RRGGBBAA` hashes. You must enclose 67 | them in a string, e.g. `'#12345678'`, or just use the `rgba()` notation. 68 | - The SASS parser will also not work with several scope selector operators (`-`, 69 | `&`, `(`, `)`, `|`). You must escape these with a backslash. 70 | The same applies to scope-segments starting with a number. 71 | 72 | CSScheme will take care of removing backslashes before emitting the final 73 | conversion result. 74 | Examples can be found in the [example files](#example-files)). 75 | 76 | **Note**: Because the SASS parser does not know about the semantics of these 77 | operators, they will generally behave poorly when used in conjunction with 78 | scope nesting. 79 | 80 | 81 | Things you *must* consider additionally when using CSScheme with **stylus**: 82 | 83 | - Make sure that `stylus` is available on your PATH or adjust the path to the 84 | executable in the settings. 85 | - At-rules, like the required global `@name` must be encapsulated with 86 | `unquote()`. Example: `unquote('@name "Example StyluScheme";')` 87 | - At-rules in non-global scope **do not work**! You'd only need these for 88 | `@name` or possibly `@comment` anyway, but stylus does some weird stuff that 89 | does not translate into sane CSScheme. 90 | 91 | 92 | ### Supported Syntaxes 93 | 94 | CSScheme (the package) provides native support for CSScheme-to-`.tmTheme` 95 | conversion. Thus, all languages that compile to CSS will also work in one way or 96 | another. SCSS/SASS and stylus are automatically built from within Sublime Text, 97 | and SCSScheme even has its own syntax definition because the one from the SCSS 98 | package highlights unknown properties as invalid. Furthermore they provide 99 | snippets and completions. 100 | 101 | - Syntax highlighting for CSScheme and SCSScheme is bundled. Snippets and 102 | completions are provided for both. 103 | - For SASScheme syntax highlighting you additionally need the [Sass][] package. 104 | - For StyluScheme syntax highlighting you additionally need the [Stylus][] 105 | package. 106 | 107 | [Sass]: https://packagecontrol.io/packages/Sass 108 | [Stylus]: https://packagecontrol.io/packages/Stylus 109 | 110 | If you want to use something a different pre-processor, you can do so by 111 | converting to CSScheme externally and then do conversion from CSScheme to 112 | tmTheme from within Sublime Text. Feel free to file an issue (if there isn't one 113 | already) if you'd like built-in support for an additional pre-processor. 114 | 115 | 116 | ### Utility for Scheme Creation 117 | *(only CSScheme and SCSScheme)* 118 | 119 | #### Symbol List 120 | 121 | Just press ctrl+r (⌘r). 122 | 123 | In StyluScheme this is *somewhat* supported but since scope names are not 124 | regular html tags they don't get recognized (since I didn't bother to write a 125 | new syntax definition for stylus as well). 126 | 127 | #### Snippets 128 | 129 | - `*` (`*` ruleset) 130 | - `r` (general purpose ruleset) 131 | 132 | *only SCSScheme:* 133 | 134 | - `mixin`, `=` (short for `mixin`) 135 | - `if`, `elif`, `else` 136 | - `for` (from ... to), `fort` (from ... through) 137 | - `each` 138 | - `while` 139 | 140 | #### Completions 141 | 142 | All known properties are completed as well as the basic scopes from the 143 | [Text Mate scope naming conventions](#useful-resources) when specifying a 144 | selector. 145 | 146 | 147 | ### Useful Resources 148 | 149 | Here is a bunch of links that might help you when working on your color scheme. 150 | 151 | - [TextMate Manual - Scope Selectors](http://manual.macromates.com/en/scope_selectors) 152 | - [TextMate Manual - Scope Naming Conventions](http://manual.macromates.com/en/language_grammars.html#naming-conventions) 153 | 154 | - [SASS/SCSS](http://sass-lang.com/) 155 | - [SASS (color) function reference](http://sass-lang.com/documentation/Sass/Script/Functions.html) 156 | - [Overview of SASS functions with example colors](http://jackiebalzer.com/color) 157 | - [stylus reference](http://learnboost.github.io/stylus/) 158 | 159 | - [HSL to RGB converter](http://serennu.com/colour/hsltorgb.php) 160 | - [Color Scheme Calculator](http://serennu.com/colour/colourcalculator.php) 161 | - [Hue scales using HCL](http://vis4.net/blog/posts/avoid-equidistant-hsv-colors/) 162 | - [Multi-hued color scales](https://vis4.net/blog/posts/mastering-multi-hued-color-scales/) 163 | - [Tool for multi-hued color scales](https://vis4.net/labs/multihue/) 164 | 165 | 166 | ## Example Files 167 | 168 | I prepared two example files that are merely a proof of concept and show a few 169 | of the features that are supported. The colors itself don't make much sense and 170 | are not good on the eyes because I picked them pretty much randomly, but it 171 | gives some great insight on what is possible. 172 | 173 | - [**Example SCSScheme.scsscheme**](./Example SCSScheme.scsscheme) 174 | - [**Example StyluScheme.scsscheme**](./Example StyluScheme.styluscheme) 175 | 176 | If you would like to see a real world example, refer to the [Writerly Scheme][] 177 | by [@alehandrof][] which heavily uses SASS's `@import` to make a larger scheme 178 | more manageable. 179 | 180 | [Writerly Scheme]: https://github.com/alehandrof/Writerly 181 | [@alehandrof]: https://github.com/alehandrof 182 | 183 | 184 | ## Other Efforts for Easing Color Scheme Creation 185 | 186 | Please note that all these work directly on `.tmTheme` files. 187 | 188 | - - Cross platform Python 189 | application for editing color schemes in a GUI 190 | - - OS X App, similar to the above 191 | - - Webapp, similar to the above but 192 | with a bunch of example color schemes to preview/edit and a nice preview 193 | - - Sublime Text plugin that 194 | syncronizes 195 | -------------------------------------------------------------------------------- /completions.py: -------------------------------------------------------------------------------- 1 | import sublime 2 | import sublime_plugin 3 | 4 | 5 | from .tinycsscheme.dumper import CSSchemeDumper 6 | from .scope_data import COMPILED_HEADS 7 | 8 | from .convert import status 9 | 10 | 11 | class CSSchemeCompletionListener(sublime_plugin.EventListener): 12 | def __init__(self): 13 | properties = set() 14 | for l in CSSchemeDumper.known_properties.values(): 15 | properties |= l 16 | 17 | self.property_completions = list(("{0}\t{0}:".format(s), s + ": $1;$0") 18 | for s in properties) 19 | 20 | def get_scope(self, view, l): 21 | # Do some string math (instead of regex because fastness) 22 | _, col = view.rowcol(l) 23 | begin = view.line(l).begin() 24 | line = view.substr(sublime.Region(begin, l)) 25 | scope = line.rsplit(' ', 1)[-1] 26 | return scope.lstrip('-') 27 | 28 | def on_query_completions(self, view, prefix, locations): 29 | # Provide a selection of naming convention from TextMate and/or property names 30 | 31 | def match_sel(sel): 32 | return all(view.match_selector(l, sel) for l in locations) 33 | 34 | # Check context 35 | if not match_sel("source.csscheme - comment - string - variable"): 36 | return 37 | 38 | if match_sel("meta.ruleset"): 39 | # No nested rulesets for CSS 40 | return self.property_completions 41 | 42 | if not match_sel("meta.selector, meta.property_list - meta.property"): 43 | return 44 | 45 | scope = self.get_scope(view, locations[0]) 46 | 47 | # We can't work with different prefixes 48 | if any(self.get_scope(view, l) != scope for l in locations): 49 | return 50 | 51 | # Tokenize the current selector (only to the cursor) 52 | tokens = scope.split(".") 53 | 54 | if len(tokens) > 1: 55 | del tokens[-1] # The last token is either incomplete or empty 56 | 57 | # Browse the nodes and their children 58 | nodes = COMPILED_HEADS 59 | for i, token in enumerate(tokens): 60 | node = nodes.find(token) 61 | if not node: 62 | status("Warning: `%s` not found in scope naming conventions" 63 | % '.'.join(tokens[:i + 1])) 64 | break 65 | nodes = node.children 66 | if not nodes: 67 | break 68 | 69 | if nodes and node: 70 | return (nodes.to_completion(), sublime.INHIBIT_WORD_COMPLETIONS) 71 | else: 72 | status("No nodes available in scope naming conventions after `%s`" 73 | % '.'.join(tokens)) 74 | return # Should I inhibit here? 75 | 76 | # Triggered completion on whitespace: 77 | elif match_sel("source.csscheme.scss"): 78 | # For SCSS just return all the head nodes + property completions 79 | return self.property_completions + COMPILED_HEADS.to_completion() 80 | else: 81 | return COMPILED_HEADS.to_completion() 82 | -------------------------------------------------------------------------------- /convert.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | import sublime 4 | 5 | # Use a different name because PackageDev adds it to the path and that 6 | # takes precedence over local paths (for some reason). 7 | from .my_sublime_lib import WindowAndTextCommand 8 | from .my_sublime_lib.path import file_path_tuple 9 | from .my_sublime_lib.view import OutputPanel, get_text, set_text 10 | 11 | from .tinycsscheme import parser, dumper 12 | 13 | from . import converters 14 | from .converters import tmtheme 15 | 16 | 17 | ############################################################################### 18 | 19 | 20 | PACKAGE = __package__ 21 | DEBUG = False 22 | 23 | 24 | def settings(): 25 | """Load the settings file.""" 26 | # We can safely call this over and over because it caches internally 27 | return sublime.load_settings("CSScheme.sublime-settings") 28 | 29 | 30 | def status(msg, printonly=""): 31 | """Show a message in the statusbar and print to the console.""" 32 | sublime.status_message("%s: %s" % (PACKAGE, msg)) 33 | if printonly: 34 | msg = msg + '\n' + printonly 35 | print("[%s] %s" % (PACKAGE, msg)) 36 | 37 | 38 | ############################################################################### 39 | 40 | 41 | # Use window (and text) command to be able to call this command from both 42 | # sources (build systems are always window commands). 43 | class convert_csscheme(WindowAndTextCommand): # noqa 44 | 45 | """Convert the active CSScheme (or variant) file into a .tmTheme plist.""" 46 | 47 | def is_enabled(self): 48 | path = self.view.file_name() 49 | return bool(path) and any(conv.valid_file(path) for conv in converters.all) 50 | 51 | def run(self, edit=None): 52 | if self.view.is_dirty(): 53 | return status("Save the file first.") 54 | 55 | self.preview_opened = False 56 | in_file = self.view.file_name() 57 | in_tuple = file_path_tuple(in_file) 58 | ext = '.tmTheme' 59 | 60 | # Open up output panel and auto-finalize it when we are done 61 | with OutputPanel(self.view.window(), "csscheme") as out: 62 | 63 | # Determine our converter 64 | conv = tuple(c for c in converters.all if c.valid_file(in_file)) 65 | if len(conv) > 1: 66 | out.write_line("Found multiple contenders for conversion.\n" 67 | "If this happened to you, please tell the developer " 68 | "(me) to add code for this case. Thanks.") 69 | return 70 | elif not conv: 71 | out.write_line("Couldn't match extension against a known converter.\n" 72 | "Known extensions are: %s" 73 | % ', '.join("." + c.ext for c in converters.all)) 74 | return 75 | conv = conv[0] 76 | 77 | out.set_path(in_tuple.path) 78 | executables = settings().get("executables", {}) 79 | 80 | # Run converter 81 | text = conv.convert(out, in_file, executables) 82 | if not text: 83 | return 84 | 85 | # Preview converted css for debugging, optionally 86 | self.previewed = not settings().get('preview_compiled_css') 87 | 88 | def preview_compiled_css(): 89 | if not self.previewed: 90 | self.preview_compiled_css(text, conv, in_tuple.base_name) 91 | self.previewed = True 92 | 93 | # Parse the CSS 94 | stylesheet = parser.parse_stylesheet(text) 95 | 96 | # Do some awesome error printing action 97 | if stylesheet.errors: 98 | conv.report_parse_errors(out, in_file, text, stylesheet.errors) 99 | preview_compiled_css() 100 | return 101 | elif not stylesheet.rules: 102 | # The CSS seems to be ... empty? 103 | out.write_line("No CSS data was found") 104 | return 105 | 106 | # Check for "hidden" at-rule 107 | for i, r in enumerate(stylesheet.rules): 108 | if not r.at_keyword or r.at_keyword.strip('@') != 'hidden': 109 | continue 110 | if parser.strvalue(r.value) == 'true': 111 | ext = '.hidden-tmTheme' 112 | del stylesheet.rules[i] 113 | break 114 | else: 115 | e = dumper.DumpError(r, "Unrecognized value for 'hidden' " 116 | "at-rule, expected 'true'") 117 | conv.report_dump_error(out, in_file, text, e) 118 | preview_compiled_css() 119 | return 120 | 121 | # Dump CSS data as plist into out_file 122 | out_file = in_tuple.no_ext + ext 123 | try: 124 | dumper.dump_stylesheet_file(out_file, stylesheet) 125 | except dumper.DumpError as e: 126 | conv.report_dump_error(out, in_file, text, e) 127 | if DEBUG: 128 | import traceback 129 | traceback.print_exc() 130 | preview_compiled_css() 131 | return 132 | 133 | status("Build successful") 134 | # Open out_file 135 | if settings().get('open_after_build'): 136 | self.view.window().open_file(out_file) 137 | 138 | def preview_compiled_css(self, text, conv, base_name): 139 | if conv.ext == 'csscheme': 140 | return 141 | 142 | v = self.view.window().new_file() 143 | v.set_scratch(True) 144 | v.set_syntax_file("Packages/%s/Package/CSScheme.tmLanguage" % PACKAGE) 145 | v.set_name("Preview: %s.csscheme" % base_name) 146 | set_text(v, text) 147 | 148 | 149 | class convert_tmtheme(WindowAndTextCommand): # noqa 150 | 151 | """Convert a .tmTheme plist into a CSScheme file.""" 152 | 153 | def is_enabled(self): 154 | path = self.view.file_name() 155 | # must return a boolean 156 | return bool(path and (path.endswith(".tmTheme") 157 | or path.endswith(".hidden-tmTheme"))) 158 | 159 | def run(self, edit=None, overwrite=False, skip_names=False): 160 | path = self.view.file_name() 161 | new_path = os.path.splitext(path)[0] + '.csscheme' 162 | 163 | if not overwrite and os.path.exists(new_path): 164 | if not sublime.ok_cancel_dialog("The file %s already exists.\n" 165 | "Do you want to overwrite?" 166 | % new_path): 167 | return 168 | 169 | with OutputPanel(self.view.window(), "csscheme_tmtheme") as out: 170 | # Load the tmTheme file 171 | data = tmtheme.load(get_text(self.view), path, out) 172 | if not data: 173 | return 174 | 175 | # Convert 176 | csscheme = tmtheme.to_csscheme(data, out, skip_names, 177 | hidden=path.endswith(".hidden-tmTheme")) 178 | if not csscheme: 179 | return 180 | 181 | # View.insert respects the tab vs. spaces and tab_width settings, 182 | # whch is why we use it instead of writing to the file directly. 183 | v = self.view.window().open_file(new_path) 184 | 185 | # open_file returns an oddly behaving view that does not accept 186 | # inputs, unless invoked on a different thread e.g. using 187 | # set_timeout: https://github.com/SublimeTextIssues/Core/issues/678 188 | def continue_operation(): 189 | # The 'insert' and 'insert_snippet' commands and View.insert 190 | # respect auto-indentation rules, which we don't want. 191 | v.settings().set('auto_indent', False) 192 | set_text(v, csscheme) 193 | v.settings().erase('auto_indent') 194 | 195 | v.run_command('save') 196 | 197 | sublime.set_timeout(continue_operation, 0) 198 | -------------------------------------------------------------------------------- /converters/__init__.py: -------------------------------------------------------------------------------- 1 | """Provides various to-csscheme converters.""" 2 | 3 | import re 4 | import os 5 | import subprocess 6 | 7 | import sublime 8 | 9 | __all__ = ('all', 'CSSConverter', 'SCSSConverter', 'SASSConverter', 'StylusConverter') 10 | 11 | 12 | def swap_path_line(pattern, rel_dir): 13 | """Create a function for use with `re.sub`. 14 | 15 | Requires matches in groups 1 and 2 and also replaces absolute paths with 16 | relative where possible. 17 | """ 18 | def repl(m): 19 | # Make path relative because we don't need long paths if in same dir 20 | path = m.group(2) 21 | try: 22 | path = os.path.relpath(m.group(2), rel_dir) 23 | except ValueError: 24 | # In case the file is on a different drive 25 | pass 26 | 27 | # Don't make relative if going up more than N folders 28 | if path.startswith((".." + os.sep) * 3): 29 | path = m.group(2) 30 | return pattern % (path, m.group(1)) 31 | 32 | return repl 33 | 34 | 35 | class BaseConverter(object): 36 | 37 | """abstract base class.""" 38 | 39 | name = "" 40 | ext = "" 41 | default_executable = "" 42 | cmd_params = () 43 | 44 | @classmethod 45 | def valid_file(cls, file_path): 46 | """Test if a file is applicable for this builder. 47 | 48 | By default, matches against the class's extension. 49 | """ 50 | return file_path.endswith('.' + cls.ext) 51 | 52 | @classmethod 53 | def convert(cls, out, file_path, executables): 54 | """Convert the specified file to CSScheme and return as string. 55 | 56 | * out - output panel to write output to 57 | * file_path - file to convert 58 | * executables - dict with optional path settings 59 | """ 60 | # Just read the file when we have no executable 61 | if not cls.default_executable: 62 | try: 63 | with open(file_path) as f: 64 | return f.read() 65 | except OSError as e: 66 | out.write_line("Error reading %s:\n%s" % (file_path, e)) 67 | return 68 | 69 | # Construct command 70 | executable = cls.default_executable 71 | if cls.default_executable in executables: 72 | executable = executables[cls.default_executable] 73 | cmd = (executable,) + cls.cmd_params + (file_path,) 74 | 75 | try: 76 | # TODO fix encoding from stylus output, mainly paths 77 | process = subprocess.Popen(cmd, 78 | stdout=subprocess.PIPE, 79 | stderr=subprocess.PIPE, 80 | shell=sublime.platform() == 'windows', 81 | universal_newlines=True) 82 | stdout, stderr = process.communicate() 83 | except Exception as e: 84 | out.write_line("Error converting from %s to CSScheme:\n" 85 | "%s: %s" % (cls.name, e.__class__.__name__, e)) 86 | return 87 | 88 | # Process results 89 | if process.returncode: 90 | cls.report_convert_errors(out, file_path, process.returncode, stderr) 91 | elif not stdout: 92 | out.write_line("Unexpected error converting from %s to CSS:\nNo output" 93 | % cls.name) 94 | else: 95 | # e.g. "warn(msg)" in stylus 96 | if stderr: 97 | out.write_line(stderr) 98 | return stdout 99 | 100 | @classmethod 101 | def report_convert_errors(cls, out, file_path, returncode, stderr): 102 | out.write_line("Error(s) converting from %s to CSS, return code: %s\n" 103 | % (cls.name, returncode)) 104 | 105 | out.write_line(stderr) 106 | 107 | @classmethod 108 | def report_parse_errors(cls, out, file_path, source, errors): 109 | out.write_line("Error(s) parsing CSScheme:\n") 110 | out.set_regex(r"^(.*):(\d+):(\d+):$") 111 | for e in errors: 112 | out.write_line("%s:%s:%s:\n %s\n" 113 | % (os.path.basename(file_path), e.line, e.column, e.reason)) 114 | 115 | @classmethod 116 | def report_dump_error(cls, out, file_path, source, e): 117 | out.write_line("Error in CSScheme data:\n") 118 | out.set_regex(r"^(.*):(\d+):(\d+):$") 119 | out.write_line("%s:%s:%s:\n %s%s\n" 120 | % (os.path.basename(file_path), e.line, e.column, e.reason, e.location)) 121 | 122 | 123 | class CSSConverter(BaseConverter): 124 | 125 | """Convert CSScheme to tmTheme.""" 126 | 127 | name = "CSScheme" 128 | ext = "csscheme" 129 | 130 | 131 | class SCSSConverter(BaseConverter): 132 | 133 | """Convert SCSScheme to tmTheme.""" 134 | 135 | name = "SCSScheme" 136 | ext = "scsscheme" 137 | default_executable = "sass" 138 | cmd_params = ('--line-numbers', '--no-cache', '--scss') 139 | 140 | @classmethod 141 | def report_convert_errors(cls, out, file_path, returncode, stderr): 142 | in_dir = os.path.dirname(file_path) 143 | 144 | out.set_regex(r"^\s+in (.*?) on line (\d+)$") 145 | 146 | out.write_line("Errors converting from %s to CSS, return code: %s\n" 147 | % (cls.name, returncode)) 148 | 149 | # Swap line and path because sublime can't parse them otherwise 150 | out.write_line(re.sub(r"on line (\d+) of (.*?)$", 151 | swap_path_line(r"in %s on line %s", in_dir), 152 | stderr, 153 | flags=re.M)) 154 | 155 | @classmethod 156 | def report_parse_errors(cls, out, file_path, source, errors): 157 | in_dir = os.path.dirname(file_path) 158 | 159 | # Match our modified output 160 | out.set_regex(r"^\s*/\* (.*?), line (\d+) \*/") 161 | 162 | lines = source.split('\n') 163 | for e in errors: 164 | out.write_line("ParseError from CSS on line %d:" % e.line) 165 | 166 | printlines = cls.get_lines_till_last_lineno(lines, e.line, in_dir) 167 | for l in printlines: 168 | out.write_line(" " + l) 169 | # Mark the column where the error happened (since we don't have source code) 170 | out.write_line(" %s^" % ('-' * (e.column - 1))) 171 | out.write_line("%s\n" % (e.reason)) 172 | 173 | @classmethod 174 | def report_dump_error(cls, out, file_path, source, e): 175 | in_dir = os.path.dirname(file_path) 176 | 177 | # Match our modified output 178 | out.set_regex(r"^\s*/\* (.*?), line (\d+) \*/") 179 | 180 | lines = source.split('\n') 181 | out.write_line("Error in CSScheme data on line %d:" % e.line) 182 | 183 | printlines = cls.get_lines_till_last_lineno(lines, e.line, in_dir) 184 | for l in printlines: 185 | out.write_line(" " + l) 186 | # Mark the column where the error happened (since we don't have source code) 187 | out.write_line(" %s^" % ('-' * (e.column - 1))) 188 | out.write_line("%s%s\n" % (e.reason, e.location)) 189 | 190 | lineno_reg = re.compile(r"/\* line (\d+), (.+?) \*/", re.M) 191 | 192 | @classmethod 193 | def get_lines_till_last_lineno(cls, lines, lineno, in_dir): 194 | printlines = [] 195 | 196 | # Search for last known line number (max 20) 197 | start_dump = 0 198 | for i in range(lineno, lineno - 20, -1): 199 | if i < 0: 200 | break 201 | m = re.match(r"\s*/\* line (\d+)", lines[i]) 202 | if not m: 203 | continue 204 | 205 | start_dump = i 206 | # Swap line and path because sublime can't parse them otherwise 207 | printlines.append( 208 | cls.lineno_reg.sub(swap_path_line("/* %s, line %s */", in_dir), lines[i]) 209 | ) 210 | break 211 | 212 | if not start_dump: 213 | # Nothing found in the past lines => only store the erroneous line 214 | start_dump = lineno - 2 215 | 216 | # printlines.extend(lines[start_dump + 1:lineno]) 217 | for i in range(start_dump + 1, lineno): 218 | printlines.append(lines[i]) 219 | 220 | return printlines 221 | 222 | 223 | class SASSConverter(SCSSConverter): 224 | 225 | """Convert SASScheme to tmTheme.""" 226 | 227 | name = "SASScheme" 228 | ext = "sasscheme" 229 | cmd_params = SCSSConverter.cmd_params[:-1] 230 | 231 | 232 | class StylusConverter(SCSSConverter): 233 | 234 | """Convert Styluscheme to tmTheme.""" 235 | 236 | name = "StyluScheme" 237 | ext = "styluscheme" 238 | default_executable = "stylus" 239 | cmd_params = ('-l', '-p') 240 | 241 | lineno_reg = re.compile(r"/\* line (\d+) : (.+?) \*/", re.M) 242 | 243 | @classmethod 244 | def report_convert_errors(cls, out, *args, **kwargs): 245 | out.set_regex(r"Error: (.+?):(\d+)$") 246 | # The error is already well-formatted so we just need to print it. 247 | # If you didn't notice, this skips SCSSConverter's implementation. 248 | super(SCSSConverter, cls).report_convert_errors(out, *args, **kwargs) 249 | 250 | # For exporting 251 | all = (CSSConverter, SCSSConverter, SASSConverter, StylusConverter) 252 | -------------------------------------------------------------------------------- /converters/tmtheme.py: -------------------------------------------------------------------------------- 1 | import os 2 | from io import StringIO 3 | 4 | __all__ = ('load',) 5 | 6 | debug_base = 'Error parsing Property List "%s": %s, line %s, column %s' 7 | debug_base_2 = 'Error parsing Property List "%s": %s' 8 | file_regex = r'Error parsing Property List "(.*?)": .*?(?:, line (\d+), column (\d+))?' 9 | 10 | 11 | def load(text, path, out): 12 | """Load a tmTheme property list and write errors to an output panel. 13 | 14 | :param text: 15 | The text of the file to be parsed. 16 | :param path: 17 | The path of the file, for error output purposes. 18 | :param out: 19 | OutputPanel instance. 20 | 21 | :return: 22 | `None` if errored, the parsed data otherwise (mostly a dict). 23 | """ 24 | dirname = os.path.dirname(path) 25 | out.set_path(dirname, file_regex) 26 | if text.startswith(''): 27 | text = text[38:] 28 | 29 | try: 30 | from xml.parsers.expat import ExpatError, ErrorString 31 | except ImportError: 32 | # TODO: provide a compat plist parser as dependency 33 | # xml.parsers.expat is not available on certain Linux dists, try to use plist_parser then. 34 | # See https://github.com/SublimeText/AAAPackageDev/issues/19 35 | # Let's just hope AAAPackageDev is installed 36 | try: 37 | import plist_parser 38 | except ImportError: 39 | out.write_line("Unable to load xml.parsers.expat or plist_parser modules.\n" 40 | "Please report to the package author.") 41 | return 42 | else: 43 | out.write_line("Unable to load plistlib, using plist_parser instead\n") 44 | 45 | try: 46 | data = plist_parser.parse_string(text) 47 | except plist_parser.PropertyListParseError as e: 48 | out.write_line(debug_base_2 % (path, str(e))) 49 | else: 50 | return data 51 | else: 52 | import plistlib 53 | try: 54 | # This will try `from xml.parsers.expat import ParserCreate` 55 | # but since it is already tried above it should succeed. 56 | return plistlib.readPlistFromBytes(text.encode('utf-8')) 57 | except ExpatError as e: 58 | out.write_line(debug_base 59 | % (path, 60 | ErrorString(e.code), 61 | e.lineno, 62 | e.offset + 1) 63 | ) 64 | 65 | 66 | def to_csscheme(data, out, skip_names, hidden=False): 67 | with StringIO() as stream: 68 | if 'name' in data: 69 | stream.write('@name "%s";\n\n' % data['name']) 70 | 71 | if hidden: 72 | stream.write('@hidden true;\n\n') 73 | 74 | uuid = data.get('uuid') 75 | if uuid: 76 | stream.write('@uuid %s;\n\n' % uuid) 77 | 78 | # Search for settings item and extract the others 79 | items = data['settings'] 80 | settings = None 81 | for i, item in enumerate(items): 82 | if 'scope' not in item: 83 | if 'settings' not in item: 84 | out.write_line("Expected 'settings' key in item without scope") 85 | return 86 | settings = item['settings'] 87 | del items[i] # remove from the regular items list 88 | break 89 | 90 | if not settings: 91 | settings = [] 92 | else: 93 | settings = list(settings.items()) 94 | 95 | # Global settings 96 | settings.sort(key=lambda x: x[0].lower()) 97 | stream.write("* {") 98 | for key, value in settings: 99 | stream.write("\n\t%s: %s;" % (key, value)) 100 | stream.write("\n}") 101 | 102 | # The other items 103 | for item in items: 104 | if 'scope' not in item: 105 | out.write_line("Missing 'scope' key in item") 106 | return 107 | stream.write("\n\n%s {" % item['scope']) 108 | 109 | if not skip_names and 'name' in item: 110 | stream.write('\n\t@name "%s";' % item['name']) 111 | 112 | if 'settings' not in item: 113 | out.write_line("Missing 'settings' key in item") 114 | return 115 | for key, value in item['settings'].items(): 116 | stream.write("\n\t%s: %s;" % (key, value)) 117 | 118 | stream.write("\n}") 119 | 120 | stream.write("\n") 121 | return stream.getvalue() 122 | -------------------------------------------------------------------------------- /create_new_csscheme.py: -------------------------------------------------------------------------------- 1 | import re 2 | from textwrap import dedent 3 | 4 | import sublime_plugin 5 | 6 | 7 | PACKAGE = __package__ 8 | 9 | 10 | csscheme_snippet = dedent("""\ 11 | @name "${1:Name}"; 12 | 13 | * { 14 | background: ${2:#ddd}; 15 | foreground: ${3:#222}; 16 | 17 | caret: ${4:#fff}; 18 | lineHighlight: ${5:#12345678}; 19 | selection: ${6:#f00}; 20 | } 21 | 22 | string { 23 | foreground: ; 24 | } 25 | 26 | string punctuation.definition { 27 | foreground: ; 28 | } 29 | 30 | string.constant { 31 | foreground: ; 32 | } 33 | 34 | constant { 35 | foreground: ; 36 | } 37 | 38 | constant.numeric { 39 | foreground: ; 40 | } 41 | 42 | comment { 43 | foreground: ; 44 | fontStyle: italic; 45 | } 46 | 47 | support { 48 | foreground: ; 49 | } 50 | 51 | support.constant { 52 | foreground: ; 53 | } 54 | 55 | entity { 56 | foreground: ; 57 | } 58 | 59 | invalid { 60 | foreground: ; 61 | } 62 | 63 | invalid.illegal { 64 | background: ; 65 | } 66 | 67 | keyword { 68 | foreground: ; 69 | } 70 | 71 | storage { 72 | foreground: ; 73 | } 74 | 75 | variable, support.variable { 76 | foreground: ; 77 | } 78 | """).replace(" ", "\t") 79 | 80 | scsscheme_snippet = dedent("""\ 81 | @name "${1:Name}"; 82 | 83 | * { 84 | background: ${2:#ddd}; 85 | foreground: ${3:#222}; 86 | 87 | caret: ${4:#fff}; 88 | lineHighlight: ${5:'#12345678'}; 89 | selection: ${6:#f00}; 90 | } 91 | 92 | string { 93 | foreground: ; 94 | 95 | punctuation.definition { 96 | foreground: ; 97 | } 98 | 99 | &.constant { 100 | foreground: ; 101 | } 102 | } 103 | 104 | constant { 105 | foreground: ; 106 | 107 | &.numeric { 108 | foreground: ; 109 | } 110 | } 111 | 112 | comment { 113 | foreground: ; 114 | fontStyle: italic; 115 | } 116 | 117 | support { 118 | foreground: ; 119 | 120 | &.constant { 121 | foreground: ; 122 | } 123 | } 124 | 125 | entity { 126 | foreground: ; 127 | } 128 | 129 | invalid { 130 | foreground: ; 131 | 132 | &.illegal { 133 | background: ; 134 | } 135 | } 136 | 137 | keyword { 138 | foreground: ; 139 | } 140 | 141 | storage { 142 | foreground: ; 143 | } 144 | 145 | variable, support.variable { 146 | foreground: ; 147 | } 148 | """).replace(" ", "\t") 149 | 150 | # Do some dirty regex replaces because ... well, it's easy 151 | sasscheme_snippet = re.sub(r" \{$|\n\t*\}|;$", '', scsscheme_snippet, flags=re.M) 152 | # Does anyone actually like removing these colons? I prefer them visible 153 | styluscheme_snippet = re.sub(r":(?= )", '', sasscheme_snippet, flags=re.M) 154 | 155 | 156 | class create_csscheme(sublime_plugin.WindowCommand): # noqa 157 | snippets = dict( 158 | CSScheme=csscheme_snippet, 159 | SCSScheme=scsscheme_snippet, 160 | SASScheme=sasscheme_snippet, 161 | StyluScheme=styluscheme_snippet 162 | ) 163 | 164 | def run(self, syntax=None): 165 | if not syntax or syntax not in self.snippets: 166 | print("create_csscheme: Invalid type parameter") 167 | return 168 | 169 | v = self.window.new_file() 170 | v.set_syntax_file("Packages/%s/Package/%s.tmLanguage" 171 | % (PACKAGE, syntax)) 172 | v.run_command("insert_snippet", {"contents": self.snippets[syntax]}) 173 | -------------------------------------------------------------------------------- /dev-requirements.txt: -------------------------------------------------------------------------------- 1 | pytest==2.9.2 2 | flake8==2.6.0 3 | pytest-cov==2.2.1 4 | -------------------------------------------------------------------------------- /messages.json: -------------------------------------------------------------------------------- 1 | { 2 | "install": "messages/install.md", 3 | "0.2.0": "messages/0.2.0.md", 4 | "0.2.1": "messages/0.2.1.md", 5 | "1.0.0": "messages/1.0.0.md", 6 | "1.1.0": "messages/1.1.0.md", 7 | "1.1.1": "messages/1.1.1.md", 8 | "1.2.0": "messages/1.2.0.md", 9 | "1.3.0": "messages/1.3.0.md" 10 | } 11 | -------------------------------------------------------------------------------- /messages/0.2.0.md: -------------------------------------------------------------------------------- 1 | v0.2.0 (2014-02-24) 2 | ------------------- 3 | 4 | - Added more known_properties to check values against (#2) 5 | - Fixed errors when using functions in "unknown" properties (#1) 6 | - Fixed incorrect error messages for empty output from running `sass` 7 | - Fixed unexpected behavior from running `sass` on non-Windows 8 | -------------------------------------------------------------------------------- /messages/0.2.1.md: -------------------------------------------------------------------------------- 1 | v0.2.1 (2014-03-01) 2 | ------------------- 3 | 4 | - Added "foreground" to allowed style list properties (.g. "bracketsOptions") 5 | -------------------------------------------------------------------------------- /messages/0.3.0.md: -------------------------------------------------------------------------------- 1 | v0.3.0 (2014-03-08) 2 | ------------------- 3 | 4 | - All "package" related files were moved to a sub-directory 5 | 6 | As a result you may see a popup that the "(S)CSScheme" syntax could not be 7 | found because Sublime cached the old path. If you encounter a CSScheme file 8 | without syntax highlighting, just close and reopen it or select "Set Syntax: 9 | SCSScheme" in the command palette. 10 | 11 | 12 | - Differentiate between style and options list ("fontStyle" vs e.g. 13 | "tagsOptions") for validation (also #2) 14 | - Allow `"fontStyle": none;` for empty style list (#4) 15 | - Highlight SASS's `index` function 16 | - Fix not showing error message if a line number was not found from the compiled 17 | SCSS (within the last x lines) 18 | - Add snippets for `@for`, `@each`, `@else if`, `@else`, `@while` 19 | -------------------------------------------------------------------------------- /messages/1.0.0.md: -------------------------------------------------------------------------------- 1 | v1.0.0 (2014-08-28) 2 | ------------------- 3 | 4 | - The settings management for executable paths has been changed! 5 | If you depend on this, you'll have to revisit. 6 | - Added support for stylus! (an example file has been bundled as well) 7 | 8 | - If running a pre-compiler, the compiled result will always be shown if there 9 | was an error parsing it 10 | - Added commands to create a new csscheme file (or variation) based on templates 11 | - Added command palette entries to open the readme and settings files 12 | - DumpErrors now show the same debug output as ParseErrors 13 | - Fixed long relative path references in some situations (mainly stylus) 14 | - Fixed wrong syntax file reference with `"preview_compiled_css": true` 15 | - SASScheme files now also get a dedicated syntax which allows CSScheme to more 16 | accurately match its build system (same for stylus). This relies on the 17 | external "Sass" package. 18 | - Fixed wrong line number being displayed when an at-rule was encountered 19 | multiple times 20 | - Added punctuation scopes to auto completion (csscheme, scsscheme) 21 | 22 | 23 | The 0.3.0 release is a bit older, but I forgot to make PC show the update 24 | message, so it's included here as well: 25 | 26 | v0.3.0 (2014-03-08) 27 | ------------------- 28 | 29 | - Differentiate between style and options list ("fontStyle" vs e.g. 30 | "tagsOptions") for validation (also #2) 31 | - Allow `"fontStyle": none;` for empty style list (#4) 32 | - Highlight SASS's `index` function 33 | - Fix not showing error message if a line number was not found from the 34 | compiled SCSS (within the last x lines) 35 | - Added snippets for `@for`, `@each`, `@else if`, `@else`, `@while` 36 | - All "package" related files were moved to a sub-directory 37 | -------------------------------------------------------------------------------- /messages/1.1.0.md: -------------------------------------------------------------------------------- 1 | v1.1.0 (2015-02-14) 2 | ------------------- 3 | 4 | - ST2 support has been removed! Old releases are still available but 5 | development will continue only for ST3. 6 | - Added command to convert from tmTheme to CSScheme ("CSScheme: Convert to 7 | CSScheme") (#8) 8 | - Changed hyphen escape sequence for SASS/SCSS from `'-'` to `\-`, which works 9 | with the current SASS parser (#7) 10 | - Fixed a bug where uuids with leading zeros were not recognized 11 | -------------------------------------------------------------------------------- /messages/1.1.1.md: -------------------------------------------------------------------------------- 1 | v1.1.1 (2015-03-17) 2 | ------------------- 3 | 4 | - 'shadowWidth' is now a known property (as integer) and its value is checked 5 | - Literal integers are now supported, such as `shadowWidth: 10;` 6 | - Completions have received an additional tab trigger to skip the semi colon 7 | -------------------------------------------------------------------------------- /messages/1.2.0.md: -------------------------------------------------------------------------------- 1 | v1.2.0 (2015-08-28) 2 | ------------------- 3 | 4 | - You can create `.hidden-tmTheme` files by adding a global `@hidden: true;` 5 | rule to the source. The rule is consumed and the output file's extension 6 | adjusted. (#9) 7 | - The global `@name` rule is now optional. Sublime Text doesn't use it anyway. 8 | - The built example schemes are now hidden, so they don't pop up in the 9 | "Prefereces > Color Scheme" menu anymore 10 | -------------------------------------------------------------------------------- /messages/1.3.0.md: -------------------------------------------------------------------------------- 1 | v1.3.0 (2016-06-23) 2 | ------------------- 3 | 4 | - Prevent `sass` executable from building caches. They were put in weird places 5 | and generally annoying. 6 | - Syntax highlighting changes to CSScheme and SCSScheme 7 | * Multiple scopes have been changed to follow (yet-to-be-specified) 8 | conventions 9 | * Highlighting of all scope selector operators has been added 10 | * Other minor tweaks 11 | - Allow backslash-escaping of any character, specifically for SASS 12 | compatibility with selector operators and scope-segments starting with 13 | numbers (#11) 14 | - Support for the old `'-'` escape sequence has been removed 15 | - `.hidden-tmTheme` files can now also be converted to `.csscheme` 16 | - Added a build system for tmTheme-to-CSScheme conversion 17 | -------------------------------------------------------------------------------- /messages/install.md: -------------------------------------------------------------------------------- 1 | Thanks for installing CSScheme. 2 | 3 | I'm not a fan of duplicated code (or text in this matter), so please check out the readme on the github repo if this is your first installation: 4 | 5 | https://github.com/FichteFoll/CSScheme/blob/master/README.md#usage 6 | 7 | (Alternatively: "Preferences > Package Settings > CSScheme > README") 8 | -------------------------------------------------------------------------------- /my_sublime_lib/LICENSE.txt: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FichteFoll/CSScheme/6575b53d2c40a64839f86e624af33bf86f6b8e34/my_sublime_lib/LICENSE.txt -------------------------------------------------------------------------------- /my_sublime_lib/__init__.py: -------------------------------------------------------------------------------- 1 | from sublime_plugin import WindowCommand, TextCommand 2 | import sublime 3 | 4 | __all__ = ['ST2', 'ST3', 'WindowAndTextCommand', 'Settings', 'FileSettings'] 5 | 6 | ST2 = sublime.version().startswith('2') 7 | ST3 = not ST2 8 | 9 | 10 | class WindowAndTextCommand(WindowCommand, TextCommand): 11 | """A class to derive from when using a Window- and a TextCommand in one 12 | class (e.g. when you make a build system that should/could also be called 13 | from the command palette with the view in its focus). 14 | 15 | Defines both self.view and self.window. 16 | 17 | Be careful that self.window may be ``None`` when called as a 18 | TextCommand because ``view.window()`` is not really safe and will 19 | fail in quite a few cases. Since the compromise of using 20 | ``sublime.active_window()`` in that case is not wanted by every 21 | command I refused from doing so. Thus, the command's on duty to check 22 | whether the window is valid. 23 | 24 | Since this class derives from both Window- and a TextCommand it is also 25 | callable with the known methods, like 26 | ``window.run_command("window_and_text")``. 27 | I defined a dummy ``run`` method to prevent parameters from raising an 28 | exception so this command call does exactly nothing. 29 | Still a better method than having the parent class (the command you 30 | will define) derive from three classes with the limitation that this 31 | class must be the first one (the *Command classes do not use super() 32 | for multi-inheritance support; neither do I but apparently I have 33 | reasons). 34 | """ 35 | def __init__(self, param): 36 | # no super() call! this would get the references confused 37 | if isinstance(param, sublime.Window): 38 | self.window = param 39 | self._window_command = True # probably called from build system 40 | self.typ = WindowCommand 41 | elif isinstance(param, sublime.View): 42 | self.view = param 43 | self._window_command = False 44 | self.typ = TextCommand 45 | else: 46 | raise TypeError("Something really bad happened and you are responsible") 47 | 48 | self._update_members() 49 | 50 | def _update_members(self): 51 | if self._window_command: 52 | self.view = self.window.active_view() 53 | else: 54 | self.window = self.view.window() 55 | 56 | def run_(self, *args): 57 | """Wraps the other run_ method implementations from sublime_plugin. 58 | Required to update the self.view and self.window variables. 59 | """ 60 | self._update_members() 61 | # Obviously `super` does not work here 62 | self.typ.run_(self, *args) 63 | 64 | 65 | class Settings(object): 66 | """Helper class for accessing sublime.Settings' values. 67 | 68 | Settings(settings, none_erases=False) 69 | 70 | * settings (sublime.Settings) 71 | Should be self-explanatory. 72 | 73 | * none_erases (bool, optional) 74 | Iff ``True`` a setting's key will be erased when setting it to 75 | ``None``. This only has a meaning when the key you erase is 76 | defined in a parent Settings collection which would be 77 | retrieved in that case. 78 | 79 | Defines the default methods for sublime.Settings: 80 | 81 | get(key, default=None) 82 | set(key, value) 83 | erase(key) 84 | has(key) 85 | add_on_change(key, on_change) 86 | clear_on_change(key, on_change) 87 | 88 | http://www.sublimetext.com/docs/2/api_reference.html#sublime.Settings 89 | 90 | If ``none_erases == True`` you can erase a key when setting it to 91 | ``None``. This only has a meaning when the key you erase is defined in 92 | a parent Settings collection which would be retrieved in that case. 93 | 94 | The following methods can be used to retrieve a setting's value: 95 | 96 | value = self.get('key', default) 97 | value = self['key'] 98 | value = self.key_without_spaces 99 | 100 | The following methods can be used to set a setting's value: 101 | 102 | self.set('key', value) 103 | self['key'] = value 104 | self.key_without_spaces = value 105 | 106 | The following methods can be used to erase a key in the setting: 107 | 108 | self.erase('key') 109 | self.set('key', None) or similar # iff ``none_erases == True`` 110 | del self.key_without_spaces 111 | 112 | ! Important: 113 | Don't use the attribute method with one of these keys; ``dir(Settings)``: 114 | 115 | ['__class__', '__delattr__', '__dict__', '__doc__', '__format__', 116 | '__getattr__', '__getattribute__', '__getitem__', '__hash__', 117 | '__init__', '__module__', '__new__', '__reduce__', '__reduce_ex__', 118 | '__repr__', '__setattr__', '__setitem__', '__sizeof__', '__str__', 119 | '__subclasshook__', '__weakref__', 120 | 121 | '_none_erases', '_s', '_settable_attributes', 122 | 123 | 'add_on_change', 'clear_on_change', 124 | 'erase', 'get', 'has', 'set'] 125 | 126 | Getting will return the respective function/value, setting will do 127 | nothing. Setting of _leading_underline_values from above will result in 128 | unpredictable behavior. Please don't do this! And re-consider even when 129 | you know what you're doing. 130 | """ 131 | _none_erases = False 132 | _s = None 133 | _settable_attributes = ('_s', '_none_erases') # allow only setting of these attributes 134 | 135 | def __init__(self, settings, none_erases=False): 136 | if not isinstance(settings, sublime.Settings): 137 | raise ValueError("Not an instance of sublime.Settings") 138 | self._s = settings 139 | self._none_erases = none_erases 140 | 141 | def get(self, key, default=None): 142 | """Returns the named setting, or ``default`` if it's not defined. 143 | """ 144 | return self._s.get(key, default) 145 | 146 | def set(self, key, value): 147 | """Sets the named setting. Only primitive types, lists, and 148 | dictionaries are accepted. 149 | Erases the key iff ``value is None``. 150 | """ 151 | if value is None and self._none_erases: 152 | self.erase(key) 153 | else: 154 | self._s.set(key, value) 155 | 156 | def erase(self, key): 157 | """Removes the named setting. Does not remove it from any parent Settings. 158 | """ 159 | self._s.erase(key) 160 | 161 | def has(self, key): 162 | """Returns true iff the named option exists in this set of Settings or 163 | one of its parents. 164 | """ 165 | return self._s.has(key) 166 | 167 | def add_on_change(self, key, on_change): 168 | """Register a callback to be run whenever the setting with this key in 169 | this object is changed. 170 | """ 171 | self._s.add_on_change(key, on_change) 172 | 173 | def clear_on_change(self, key, on_change): 174 | """Remove all callbacks registered with the given key. 175 | """ 176 | self._s.clear_on_change(key, on_change) 177 | 178 | def __getitem__(self, key): 179 | """self[key]""" 180 | return self.get(key) 181 | 182 | def __setitem__(self, key, value): 183 | """self[key] = value""" 184 | self.set(key, value) 185 | 186 | def __getattr__(self, key): 187 | """self.key_without_spaces""" 188 | return self.get(key) 189 | 190 | def __setattr__(self, key, value): 191 | """self.key_without_spaces = value""" 192 | if key in self._settable_attributes: 193 | object.__setattr__(self, key, value) 194 | else: 195 | self.set(key, value) 196 | 197 | def __delattr__(self, key): 198 | """del self.key_without_spaces""" 199 | if key in dir(self): 200 | return 201 | else: 202 | self.erase(key) 203 | 204 | 205 | class FileSettings(Settings): 206 | """Helper class for accessing sublime.Settings' values. 207 | 208 | Derived from sublime_lib.Settings. Please also read the documentation 209 | there. 210 | 211 | FileSettings(name, none_erases=False) 212 | 213 | * name (str) 214 | The file name that's passed to sublime.load_settings(). 215 | 216 | * none_erases (bool, optional) 217 | Iff ``True`` a setting's key will be erased when setting it to 218 | ``None``. This only has a meaning when the key you erase is 219 | defined in a parent Settings collection which would be 220 | retrieved in that case. 221 | 222 | Defines the following extra methods: 223 | 224 | save() 225 | Flushes in-memory changes to the disk 226 | 227 | See: sublime.save_settings(name) 228 | 229 | Adds these attributes to the list of unreferable attribute names for 230 | settings: 231 | 232 | ['_name', 'save'] 233 | 234 | Please compare with the list from sublime_lib.Settings or 235 | ``dir(FileSettings)``. 236 | """ 237 | _name = "" 238 | _settable_attributes = ('_s', '_name', '_none_erases') # allow only setting of these attributes 239 | 240 | def __init__(self, name, none_erases=False): 241 | settings = sublime.load_settings(name) 242 | if not settings: 243 | raise ValueError('Could not create settings from name "%s"' % name) 244 | self._name = name 245 | super(FileSettings, self).__init__(settings, none_erases) 246 | 247 | def save(self): 248 | sublime.save_settings(self._name) 249 | -------------------------------------------------------------------------------- /my_sublime_lib/constants.py: -------------------------------------------------------------------------------- 1 | KEY_UP = "up" 2 | KEY_DOWN = "down" 3 | KEY_RIGHT = "right" 4 | KEY_LEFT = "left" 5 | KEY_INSERT = "insert" 6 | KEY_HOME = "home" 7 | KEY_END = "end" 8 | KEY_PAGEUP = "pageup" 9 | KEY_PAGEDOWN = "pagedown" 10 | KEY_BACKSPACE = "backspace" 11 | KEY_DELETE = "delete" 12 | KEY_TAB = "tab" 13 | KEY_ENTER = "enter" 14 | KEY_PAUSE = "pause" 15 | KEY_ESCAPE = "escape" 16 | KEY_SPACE = "space" 17 | KEY_KEYPAD0 = "keypad0" 18 | KEY_KEYPAD1 = "keypad1" 19 | KEY_KEYPAD2 = "keypad2" 20 | KEY_KEYPAD3 = "keypad3" 21 | KEY_KEYPAD4 = "keypad4" 22 | KEY_KEYPAD5 = "keypad5" 23 | KEY_KEYPAD6 = "keypad6" 24 | KEY_KEYPAD7 = "keypad7" 25 | KEY_KEYPAD8 = "keypad8" 26 | KEY_KEYPAD9 = "keypad9" 27 | KEY_KEYPAD_PERIOD = "keypad_period" 28 | KEY_KEYPAD_DIVIDE = "keypad_divide" 29 | KEY_KEYPAD_MULTIPLY = "keypad_multiply" 30 | KEY_KEYPAD_MINUS = "keypad_minus" 31 | KEY_KEYPAD_PLUS = "keypad_plus" 32 | KEY_KEYPAD_ENTER = "keypad_enter" 33 | KEY_CLEAR = "clear" 34 | KEY_F1 = "f1" 35 | KEY_F2 = "f2" 36 | KEY_F3 = "f3" 37 | KEY_F4 = "f4" 38 | KEY_F5 = "f5" 39 | KEY_F6 = "f6" 40 | KEY_F7 = "f7" 41 | KEY_F8 = "f8" 42 | KEY_F9 = "f9" 43 | KEY_F10 = "f10" 44 | KEY_F11 = "f11" 45 | KEY_F12 = "f12" 46 | KEY_F13 = "f13" 47 | KEY_F14 = "f14" 48 | KEY_F15 = "f15" 49 | KEY_F16 = "f16" 50 | KEY_F17 = "f17" 51 | KEY_F18 = "f18" 52 | KEY_F19 = "f19" 53 | KEY_F20 = "f20" 54 | KEY_SYSREQ = "sysreq" 55 | KEY_BREAK = "break" 56 | KEY_CONTEXT_MENU = "context_menu" 57 | KEY_BROWSER_BACK = "browser_back" 58 | KEY_BROWSER_FORWARD = "browser_forward" 59 | KEY_BROWSER_REFRESH = "browser_refresh" 60 | KEY_BROWSER_STOP = "browser_stop" 61 | KEY_BROWSER_SEARCH = "browser_search" 62 | KEY_BROWSER_FAVORITES = "browser_favorites" 63 | KEY_BROWSER_HOME = "browser_home" 64 | -------------------------------------------------------------------------------- /my_sublime_lib/edit.py: -------------------------------------------------------------------------------- 1 | # edit.py, courtesy of @lunixbochs (https://github.com/lunixbochs) 2 | # and slightly modified 3 | """Abstraction for edit objects in ST2 and ST3 4 | 5 | All methods on "edit" create an edit step. When leaving the `with` block, all 6 | the steps are executed one by one. 7 | 8 | Be careful: All other code in the with block is still executed! If a method on 9 | edit depends on something you do based on a previous method on edit, you 10 | should use the second method. However, using `edit.callback` or pass a 11 | function as an argument you can circumvent that if it's only small things. The 12 | function will be called with the parameters `view` and `edit` when processing 13 | the edit group. 14 | 15 | Usage 1: 16 | with Edit(view) as edit: 17 | edit.insert(0, "text") 18 | edit.replace(reg, "replacement") 19 | edit.erase(lambda v,e: sublime.Region(0, v.size())) 20 | # OR 21 | # edit.callback(lambda v,e: v.erase(e, sublime.Region(0, v.size()))) 22 | 23 | Usage 2: 24 | def do_ed(view, edit): 25 | edit.erase() 26 | view.insert(edit, 0, "text") 27 | view.sel().clear() 28 | view.sel().add(sublime.Region(0, 4)) 29 | edit.replace(reg, "replacement") 30 | 31 | Edit.call(do_ed) 32 | 33 | Available methods: 34 | Note: Any of these parameters can be a function which will be called with 35 | (optional) parameters `view` and `edit` when processing the edit group. 36 | Example callbacks: 37 | `lambda: 1` 38 | `lambda v: v.size()` 39 | `lambda v, e: v.erase(e, reg)` 40 | 41 | insert(point, string) 42 | view.insert(edit, point, string) 43 | 44 | append(point, string) 45 | view.insert(edit, view.size(), string) 46 | 47 | erase(region) 48 | view.erase(edit, region) 49 | 50 | replace(region, string) 51 | view.replace(edit, region, string) 52 | 53 | callback(func) 54 | func(view, edit) 55 | 56 | """ 57 | 58 | import inspect 59 | import sublime 60 | import sublime_plugin 61 | 62 | from . import ST2 63 | 64 | try: 65 | sublime.edit_storage 66 | except AttributeError: 67 | sublime.edit_storage = {} 68 | 69 | 70 | def run_callback(func, *args, **kwargs): 71 | spec = inspect.getfullargspec(func) 72 | 73 | args = args[:len(spec.args) or 0] 74 | if not spec.varargs: 75 | kwargs = {} 76 | 77 | return func(*args, **kwargs) 78 | 79 | 80 | class EditStep: 81 | def __init__(self, cmd, *args): 82 | self.cmd = cmd 83 | self.args = args 84 | 85 | def run(self, view, edit): 86 | if self.cmd == 'callback': 87 | return run_callback(self.args[0], view, edit) 88 | 89 | funcs = { 90 | 'insert': view.insert, 91 | 'erase': view.erase, 92 | 'replace': view.replace, 93 | } 94 | func = funcs.get(self.cmd) 95 | if func: 96 | args = self.resolve_args(view, edit) 97 | func(edit, *args) 98 | 99 | def resolve_args(self, view, edit): 100 | args = [] 101 | for arg in self.args: 102 | if callable(arg): 103 | arg = run_callback(arg, view, edit) 104 | args.append(arg) 105 | return args 106 | 107 | 108 | class Edit: 109 | def __init__(self, view, func=None): 110 | self.view = view 111 | self.steps = [] 112 | 113 | def __nonzero__(self): 114 | return bool(self.steps) 115 | 116 | __bool__ = __nonzero__ # Python 3 equivalent 117 | 118 | def step(self, cmd, *args): 119 | step = EditStep(cmd, *args) 120 | self.steps.append(step) 121 | 122 | def insert(self, point, string): 123 | self.step('insert', point, string) 124 | 125 | def append(self, string): 126 | self.step('insert', lambda v: v.size(), string) 127 | 128 | def erase(self, region): 129 | self.step('erase', region) 130 | 131 | def replace(self, region, string): 132 | self.step('replace', region, string) 133 | 134 | @classmethod 135 | def call(cls, view, func): 136 | if not (func and callable(func)): 137 | return 138 | 139 | with cls(view) as edit: 140 | edit.callback(func) 141 | 142 | def callback(self, func): 143 | self.step('callback', func) 144 | 145 | def run(self, view, edit): 146 | for step in self.steps: 147 | step.run(view, edit) 148 | 149 | def __enter__(self): 150 | return self 151 | 152 | def __exit__(self, type, value, traceback): 153 | view = self.view 154 | if ST2: 155 | edit = view.begin_edit() 156 | self.run(view, edit) 157 | view.end_edit(edit) 158 | else: 159 | key = str(hash(tuple(self.steps))) 160 | sublime.edit_storage[key] = self 161 | view.run_command('sl_apply_edit', {'key': key}) 162 | 163 | 164 | if not ST2: 165 | # Changed command name to not clash with other variations of this file 166 | class SlApplyEdit(sublime_plugin.TextCommand): 167 | def run(self, edit, key): 168 | sublime.edit_storage.pop(key).run(self.view, edit) 169 | 170 | # Make command known to sublime_command despite not being loaded by it 171 | sublime_plugin.text_command_classes.append(SlApplyEdit) 172 | 173 | # Make the command unloadable 174 | plugins = [SlApplyEdit] 175 | -------------------------------------------------------------------------------- /my_sublime_lib/path.py: -------------------------------------------------------------------------------- 1 | """A collection of useful functions related to paths, ST- and non-ST-related. 2 | 3 | Also has some ST-specific file extensions as "constants". 4 | """ 5 | 6 | import os 7 | import re 8 | import inspect 9 | from collections import namedtuple 10 | 11 | import sublime 12 | 13 | __all__ = ( 14 | "FTYPE_EXT_KEYMAP", 15 | "FTYPE_EXT_COMPLETIONS", 16 | "FTYPE_EXT_SNIPPET", 17 | "FTYPE_EXT_BUILD", 18 | "FTYPE_EXT_SETTINGS", 19 | "FTYPE_EXT_TMPREFERENCES", 20 | "FTYPE_EXT_TMLANGUAGE", 21 | "root_at_packages", 22 | "data_path", 23 | "root_at_data", 24 | "file_path_tuple", 25 | "get_module_path", 26 | "get_package_name" 27 | ) 28 | 29 | 30 | FTYPE_EXT_KEYMAP = ".sublime-keymap" 31 | FTYPE_EXT_COMPLETIONS = ".sublime-completions" 32 | FTYPE_EXT_SNIPPET = ".sublime-snippet" 33 | FTYPE_EXT_BUILD = ".sublime-build" 34 | FTYPE_EXT_SETTINGS = ".sublime-settings" 35 | FTYPE_EXT_TMPREFERENCES = ".tmPreferences" 36 | FTYPE_EXT_TMLANGUAGE = ".tmLanguage" 37 | 38 | 39 | def root_at_packages(*leafs): 40 | """Combine leafs with path to Sublime's Packages folder. 41 | 42 | Requires the API to finish loading on ST3. 43 | """ 44 | # If we really need to, we dan extract the packages path from sys.path (ST3) 45 | return os.path.join(sublime.packages_path(), *leafs) 46 | 47 | 48 | def data_path(): 49 | """Extract Sublime Text's data path from the packages path. 50 | 51 | Requires the API to finish loading on ST3. 52 | """ 53 | return os.path.dirname(sublime.packages_path()) 54 | 55 | 56 | def root_at_data(*leafs): 57 | """Combine leafs with Sublime's ``Data`` folder. 58 | 59 | Requires the API to finish loading on ST3. 60 | """ 61 | return os.path.join(data_path(), *leafs) 62 | 63 | 64 | FilePath = namedtuple("FilePath", "file_path path file_name base_name ext no_ext") 65 | 66 | 67 | def file_path_tuple(file_path): 68 | """Create a namedtuple with: file_path, path, file_name, base_name, ext, no_ext.""" 69 | path, file_name = os.path.split(file_path) 70 | base_name, ext = os.path.splitext(file_name) 71 | return FilePath( 72 | file_path, 73 | path, 74 | file_name, 75 | base_name, 76 | ext, 77 | no_ext=os.path.join(path, base_name) 78 | ) 79 | 80 | 81 | def get_module_path(_file_=None): 82 | """Return a tuple with the normalized module path plus a boolean. 83 | 84 | * _file_ (optional) 85 | The value of `__file__` in your module. 86 | If omitted, `get_caller_frame()` will be used instead which usually works. 87 | 88 | Return: (normalized_module_path, archived) 89 | `normalized_module_path` 90 | What you usually refer to when using Sublime API, without `.sublime-package` 91 | `archived` 92 | True, when in an archive 93 | """ 94 | if _file_ is None: 95 | _file_ = get_caller_frame().f_globals['__file__'] 96 | 97 | dir_name = os.path.dirname(os.path.abspath(_file_)) 98 | # Check if we are in an archived package 99 | if int(sublime.version()) < 3000 or not dir_name.endswith(".sublime-package"): 100 | return dir_name, False 101 | 102 | # We are in a .sublime-package and need to normalize the path 103 | virtual_path = re.sub(r"(?:Installed )?Packages([\\/][^\\/]+)\.sublime-package(?=[\\/]|$)", 104 | r"Packages\1", dir_name) 105 | return virtual_path, True 106 | 107 | 108 | def get_package_path(_file_=None): 109 | """Get the path to the current Sublime Text package. 110 | 111 | Parameters are the same as for `get_module_path`. 112 | """ 113 | if _file_ is None: 114 | _file_ = get_caller_frame().f_globals['__file__'] 115 | 116 | mpath = get_module_path(_file_)[0] 117 | 118 | # There probably is a better way for this, but it works 119 | while not os.path.dirname(mpath).endswith('Packages'): 120 | if len(mpath) <= 3: 121 | return None 122 | # We're not in a top-level plugin. 123 | # If this was ST2 we could easily use sublime.packages_path(), but ... 124 | mpath = os.path.dirname(mpath) 125 | 126 | return mpath 127 | 128 | 129 | def get_package_name(_file_=None): 130 | """`return os.path.split(get_package_path(_file_))[1]`.""" 131 | if _file_ is None: 132 | _file_ = get_caller_frame().f_globals['__file__'] 133 | 134 | return os.path.split(get_package_path(_file_))[1] 135 | 136 | 137 | def get_caller_frame(i=1): 138 | """Get the caller's frame (utilizing the inspect module). 139 | 140 | You can adjust `i` to find the i-th caller, default is 1. 141 | """ 142 | # We can't use inspect.stack()[1 + i][1] for the file name because ST sets 143 | # that to a different value when inside a zip archive. 144 | return inspect.stack()[1 + i][0] 145 | -------------------------------------------------------------------------------- /my_sublime_lib/view/__init__.py: -------------------------------------------------------------------------------- 1 | # for importing the module 2 | from .output_panel import OutputPanel 3 | from ._view import * 4 | from ._view import __all__ as vall 5 | 6 | __all__ = ['OutputPanel'] + vall[:] 7 | -------------------------------------------------------------------------------- /my_sublime_lib/view/_view.py: -------------------------------------------------------------------------------- 1 | from contextlib import contextmanager 2 | 3 | from sublime import Region, View 4 | 5 | from .. import Settings 6 | from ..edit import Edit 7 | 8 | __all__ = ['ViewSettings', 'unset_read_only', 'append', 'clear', 'set_text', 9 | 'has_sels', 'has_file_ext', 'base_scope', 'rowcount', 'rowwidth', 10 | 'relative_point', 'coorded_region', 'coorded_substr', 'get_text', 11 | 'get_viewport_point', 'get_viewport_coords', 'set_viewport', 12 | 'extract_selector'] 13 | 14 | 15 | # TODO remove 16 | class ViewSettings(Settings): 17 | """Helper class for accessing settings' values from views. 18 | 19 | Derived from sublime_lib.Settings. Please also read the documentation 20 | there. 21 | 22 | ViewSettings(view, none_erases=False) 23 | 24 | * view (sublime.View) 25 | Forwarding ``view.settings()``. 26 | 27 | * none_erases (bool, optional) 28 | Iff ``True`` a setting's key will be erased when setting it to 29 | ``None``. This only has a meaning when the key you erase is defined 30 | in a parent Settings collection which would be retrieved in that 31 | case. 32 | """ 33 | def __init__(self, view, none_erases=False): 34 | if not isinstance(view, View): 35 | raise ValueError("Invalid view") 36 | settings = view.settings() 37 | if not settings: 38 | raise ValueError("Could not resolve view.settings()") 39 | super(ViewSettings, self).__init__(settings, none_erases) 40 | 41 | 42 | @contextmanager 43 | def unset_read_only(view): 44 | """Context manager to make sure a view writable if it is read-only. 45 | If the view is not read-only it will just leave it untouched. 46 | 47 | Yields a boolean indicating whether the view was read-only before or 48 | not. This has limited use. 49 | 50 | Examples: 51 | ... 52 | with unset_read_only(view): 53 | ... 54 | ... 55 | """ 56 | read_only_before = view.is_read_only() 57 | if read_only_before: 58 | view.set_read_only(False) 59 | 60 | yield read_only_before 61 | 62 | if read_only_before: 63 | view.set_read_only(True) 64 | 65 | 66 | def append(view, text, scroll=None): 67 | """Appends text to `view`. Won't work if the view is read-only. 68 | 69 | The `scroll` parameter may be one of these values: 70 | 71 | True: Always scroll to the end of the view. 72 | False: Don't scroll. 73 | None: Scroll only if the selecton is already at the end. 74 | """ 75 | size = view.size() 76 | scroll = scroll or (scroll is not False and len(view.sel()) == 1 and 77 | view.sel()[0] == Region(size)) 78 | 79 | with Edit(view) as edit: 80 | edit.insert(size, text) 81 | 82 | if scroll: 83 | view.show(view.size()) 84 | 85 | 86 | def clear(view): 87 | """Removes all the text in ``view``. Won't work if the view is read-only. 88 | """ 89 | with Edit(view) as edit: 90 | edit.erase(Region(0, view.size())) 91 | 92 | 93 | def set_text(view, text, scroll=False): 94 | """Replaces the entire content of view with the text specified. 95 | 96 | `scroll` parameter specifies whether the view should be scrolled to the end. 97 | """ 98 | 99 | with Edit(view) as edit: 100 | edit.erase(Region(0, view.size())) 101 | edit.insert(0, text) 102 | 103 | if scroll: 104 | view.show(view.size()) 105 | else: 106 | view.sel().clear() 107 | view.sel().add(Region(0, 0)) 108 | 109 | 110 | def has_sels(view): 111 | """Returns `True` if `view` has one selection or more. 112 | """ 113 | return len(view.sel()) > 0 114 | 115 | 116 | def has_file_ext(view, ext): 117 | """Returns `True` if `view` has file extension `ext`. 118 | `ext` may be specified with or without leading ".". 119 | """ 120 | if not view.file_name() or not ext.strip().replace('.', ''): 121 | return False 122 | 123 | if not ext.startswith('.'): 124 | ext = '.' + ext 125 | 126 | return view.file_name().endswith(ext) 127 | 128 | 129 | def base_scope(view): 130 | """Returns the view's base scope. 131 | """ 132 | return view.scope_name(0).split(' ', 1)[0] 133 | 134 | 135 | def rowcount(view): 136 | """Returns the 1-based number of rows in ``view``. 137 | """ 138 | return view.rowcol(view.size())[0] + 1 139 | 140 | 141 | def rowwidth(view, row): 142 | """Returns the 1-based number of characters of ``row`` in ``view``. 143 | """ 144 | return view.rowcol(view.line(view.text_point(row, 0)).end())[1] + 1 145 | 146 | 147 | def relative_point(view, row=0, col=0, p=None): 148 | """Returns a point (int) to the given coordinates. 149 | 150 | Supports relative (negative) parameters and checks if they are in the 151 | bounds (other than `View.text_point()`). 152 | 153 | If p (indexable -> `p[0]`, `len(p) == 2`; preferrably a tuple) is 154 | specified, row and col parameters are overridden. 155 | """ 156 | if p is not None: 157 | if len(p) != 2: 158 | raise TypeError("Coordinates have 2 dimensions, not %d" % len(p)) 159 | (row, col) = p 160 | 161 | # shortcut 162 | if row == -1 and col == -1: 163 | return view.size() 164 | 165 | # calc absolute coords and check if coords are in the bounds 166 | rowc = rowcount(view) 167 | if row < 0: 168 | row = max(rowc + row, 0) 169 | else: 170 | row = min(row, rowc - 1) 171 | 172 | roww = rowwidth(view, row) 173 | if col < 0: 174 | col = max(roww + col, 0) 175 | else: 176 | col = min(col, roww - 1) 177 | 178 | return view.text_point(row, col) 179 | 180 | 181 | def coorded_region(view, reg1=None, reg2=None, rel=None): 182 | """Turn two coordinate pairs into a region. 183 | 184 | The pairs are checked for boundaries by `relative_point`. 185 | 186 | You may also supply a `rel` parameter which will determine the 187 | Region's end point relative to `reg1`, as a pair. The pairs are 188 | supposed to be indexable and have a length of 2. Tuples are preferred. 189 | 190 | Defaults to the whole buffer (`reg1=(0, 0), reg2=(-1, -1)`). 191 | 192 | Examples: 193 | coorded_region(view, (20, 0), (22, -1)) # normal usage 194 | coorded_region(view, (20, 0), rel=(2, -1)) # relative, works because 0-1=-1 195 | coorded_region(view, (22, 6), rel=(2, 15)) # relative, ~ more than 3 lines, 196 | # if line 25 is long enough 197 | 198 | """ 199 | reg1 = reg1 or (0, 0) 200 | if rel: 201 | reg2 = (reg1[0] + rel[0], reg1[1] + rel[1]) 202 | else: 203 | reg2 = reg2 or (-1, -1) 204 | 205 | p1 = relative_point(view, p=reg1) 206 | p2 = relative_point(view, p=reg2) 207 | return Region(p1, p2) 208 | 209 | 210 | def coorded_substr(view, reg1=None, reg2=None, rel=None): 211 | """Returns the string of two coordinate pairs forming a region. 212 | 213 | The pairs are supporsed to be indexable and have a length of 2. 214 | Tuples are preferred. 215 | 216 | Defaults to the whole buffer. 217 | 218 | For examples, see `coorded_region`. 219 | """ 220 | return view.substr(coorded_region(view, reg1, reg2)) 221 | 222 | 223 | def get_text(view): 224 | """Returns the whole string of a buffer. Alias for `coorded_substr(view)`. 225 | """ 226 | return coorded_substr(view) 227 | 228 | 229 | def get_viewport_point(view): 230 | """Returns the text point of the current viewport. 231 | """ 232 | return view.layout_to_text(view.viewport_position()) 233 | 234 | 235 | def get_viewport_coords(view): 236 | """Returns the text coordinates of the current viewport. 237 | """ 238 | return view.rowcol(get_viewport_point(view)) 239 | 240 | 241 | def set_viewport(view, row, col=None): 242 | """Sets the current viewport from either a text point or relative coords. 243 | 244 | set_viewport(view, 892) # point 245 | set_viewport(view, 2, 27) # coords1 246 | set_viewport(view, (2, 27)) # coords2 247 | """ 248 | if col is None: 249 | pos = row 250 | 251 | if type(row) == tuple: 252 | pos = relative_point(view, p=row) 253 | else: 254 | pos = relative_point(view, row, col) 255 | 256 | view.set_viewport_position(view.text_to_layout(pos)) 257 | 258 | 259 | def extract_selector(view, selector, point): 260 | """Works similar to view.extract_scope except that you may define the 261 | selector (scope) on your own and it does not use the point's scope by 262 | default. 263 | 264 | Example: 265 | extract_selector(view, "source string", view.sel()[0].begin()) 266 | 267 | Returns the Region for the out-most "source string" which contains the 268 | beginning of the first selection. 269 | """ 270 | regs = view.find_by_selector(selector) 271 | for reg in regs: 272 | if reg.contains(point): 273 | return reg 274 | return None 275 | -------------------------------------------------------------------------------- /my_sublime_lib/view/output_panel.py: -------------------------------------------------------------------------------- 1 | from sublime import Region, Window 2 | 3 | from ._view import ViewSettings, unset_read_only, append, clear, get_text 4 | from .. import ST3 5 | 6 | if ST3: 7 | basestring = str 8 | 9 | 10 | class OutputPanel(object): 11 | """This class represents an output panel (useful for e.g. build systems). 12 | Please note that the panel's contents will be cleared on __init__. 13 | 14 | Can be used as a context handler in `with` statement which will 15 | automatically invoke the `finish()` method. 16 | 17 | Example usage: 18 | 19 | with OutputPanel(sublime.active_window(), "test") as output: 20 | output.write_line("some testing here") 21 | 22 | 23 | OutputPanel(window, panel_name, file_regex=None, line_regex=None, path=None, 24 | read_only=True, auto_show=True) 25 | * window 26 | The window. This is usually `self.window` or 27 | `self.view.window()`, depending on the type of your command. 28 | 29 | * panel_name 30 | The panel's name, passed to `window.get_output_panel()`. 31 | 32 | * file_regex 33 | Important for Build Systems. The user can browse the errors you 34 | writewith F4 and Shift+F4 keys. The error's location is 35 | determined with 3 capturing groups: 36 | the file name, the line number and the column. 37 | The last two are optional. 38 | 39 | Example: 40 | r"Error in file "(.*?)", line (\d+), column (\d+)" 41 | 42 | * line_regex 43 | Same style as `file_regex` except that it misses the first 44 | group for the file name. 45 | 46 | If `file_regex` doesn't match on the current line, but 47 | `line_regex` exists, and it does match on the current line, 48 | then walk backwards through the buffer until a line matching 49 | file regex is found, and use these two matches 50 | to determine the file and line to go to; column is optional. 51 | 52 | * path 53 | This is only needed if you specify the file_regex param and 54 | will be used as the root dir for relative filenames when 55 | determining error locations. 56 | 57 | * read_only 58 | A boolean whether the output panel should be read only. 59 | You usually want this to be true. 60 | Can be modified with `self.view.set_read_only()` when needed. 61 | 62 | * auto_show 63 | Option if the panel should be shown when `finish()` is called and 64 | text has been added. 65 | 66 | Useful attributes: 67 | 68 | view 69 | The view handle of the output panel. Can be passed to 70 | `Edit(output.view)` to group modifications for example. 71 | 72 | Defines the following methods: 73 | 74 | set_path(path=None, file_regex=None, line_regex=None) 75 | Used to update `path`, `file_regex` and `line_regex` if 76 | they are not `None`, see the constructor for information 77 | about these parameters. 78 | 79 | The file_regex is updated automatically because it might happen 80 | that the same panel_name is used multiple times. 81 | If `file_regex` is omitted or `None` it will be reset to 82 | the latest regex specified (when creating the instance or from 83 | the last call of set_regex/path). 84 | The same applies to `line_regex`. 85 | 86 | set_regex(file_regex=None, line_regex=None) 87 | Subset of set_path. Read there for further information. 88 | 89 | write(text) 90 | Will just write appending `text` to the output panel. 91 | 92 | write_line(text='') 93 | Same as write() but inserts a newline at the end. 94 | 95 | clear() 96 | Erases all text in the output panel. 97 | 98 | show() 99 | hide() 100 | Show or hide the output panel. 101 | 102 | finish() 103 | Call this when you are done with updating the panel. 104 | Required if you want the next_result command (F4) to work. 105 | If `auto_show` is true, will also show the panel if text was added. 106 | """ 107 | def __init__(self, window, panel_name, file_regex=None, 108 | line_regex=None, path=None, read_only=True, 109 | auto_show=True): 110 | if not isinstance(window, Window): 111 | raise ValueError("window parameter is invalid") 112 | if not isinstance(panel_name, basestring): 113 | raise ValueError("panel_name must be a string") 114 | 115 | self.window = window 116 | self.panel_name = panel_name 117 | self.view = window.get_output_panel(panel_name) 118 | self.view.set_read_only(read_only) 119 | self.settings = ViewSettings(self.view) 120 | 121 | self.set_path(path, file_regex, line_regex) 122 | 123 | self.auto_show = auto_show 124 | 125 | def set_path(self, path=None, file_regex=None, line_regex=None): 126 | """Update the view's result_base_dir pattern. 127 | Only overrides the previous settings if parameters are not None. 128 | """ 129 | if path is not None: 130 | self.settings.result_base_dir = path 131 | # Also always update the file_regex 132 | self.set_regex(file_regex, line_regex) 133 | 134 | def set_regex(self, file_regex=None, line_regex=None): 135 | """Update the view's result_(file|line)_regex patterns. 136 | Only overrides the previous settings if parameters are not None. 137 | """ 138 | if file_regex is not None: 139 | self.file_regex = file_regex 140 | if hasattr(self, 'file_regex'): 141 | self.settings.result_file_regex = self.file_regex 142 | 143 | if line_regex is not None: 144 | self.line_regex = line_regex 145 | if hasattr(self, 'line_regex'): 146 | self.settings.result_line_regex = self.line_regex 147 | 148 | # Call get_output_panel again after assigning the above settings, so 149 | # that "next_result" and "prev_result" work. However, it will also clear 150 | # the view so read it before and re-write its contents afterwards. Cache 151 | # selection as well. 152 | contents = get_text(self.view) 153 | sel = self.view.sel() 154 | selections = list(sel) 155 | self.view = self.window.get_output_panel(self.panel_name) 156 | sel.clear() 157 | for reg in selections: # sel.add_all requires a `RegionSet` in ST2 158 | sel.add(reg) 159 | self.write(contents) 160 | 161 | def write(self, text): 162 | """Appends `text` to the output panel. 163 | Alias for `sublime_lib.view.append(self.view, text)` 164 | + `with unset_read_only:`. 165 | """ 166 | with unset_read_only(self.view): 167 | append(self.view, text) 168 | 169 | def write_line(self, text=''): 170 | """Appends `text` to the output panel and starts a new line. 171 | """ 172 | self.write(text + "\n") 173 | 174 | def clear(self): 175 | """Clears the output panel. 176 | Alias for `sublime_lib.view.clear(self.view)`. 177 | """ 178 | with unset_read_only(self.view): 179 | clear(self.view) 180 | 181 | def show(self): 182 | """Makes the output panel visible. 183 | """ 184 | self.window.run_command("show_panel", 185 | {"panel": "output.%s" % self.panel_name}) 186 | 187 | def hide(self): 188 | """Makes the output panel invisible. 189 | """ 190 | self.window.run_command("hide_panel", 191 | {"panel": "output.%s" % self.panel_name}) 192 | 193 | def finish(self): 194 | """Things that are required to use the output panel properly. 195 | 196 | Set the selection to the start, so that next_result will work as 197 | expected. Also shows the panel if text has been added. 198 | """ 199 | self.set_path() 200 | self.view.sel().clear() 201 | self.view.sel().add(Region(0)) 202 | if self.auto_show: 203 | if self.view.size(): 204 | self.show() 205 | else: 206 | self.hide() 207 | 208 | def __enter__(self): 209 | return self 210 | 211 | def __exit__(self, type, value, traceback): 212 | self.finish() 213 | -------------------------------------------------------------------------------- /scope_data/__init__.py: -------------------------------------------------------------------------------- 1 | import sys 2 | 3 | if sys.version_info[0] > 2: 4 | basestring = str 5 | 6 | __all__ = ["COMPILED_NODES", "COMPILED_HEADS"] 7 | 8 | 9 | # https://manual.macromates.com/en/language_grammars#naming_conventions 10 | DATA = """ 11 | comment 12 | line 13 | double-slash 14 | double-dash 15 | number-sign 16 | percentage 17 | block 18 | documentation 19 | 20 | constant 21 | numeric 22 | character 23 | escape 24 | language 25 | other 26 | 27 | entity 28 | name 29 | function 30 | type 31 | tag 32 | section 33 | other 34 | inherited-class 35 | attribute-name 36 | 37 | invalid 38 | illegal 39 | deprecated 40 | 41 | keyword 42 | control 43 | operator 44 | other 45 | 46 | markup 47 | underline 48 | link 49 | bold 50 | heading 51 | italic 52 | list 53 | numbered 54 | unnumbered 55 | quote 56 | raw 57 | other 58 | 59 | meta 60 | 61 | storage 62 | type 63 | modifier 64 | 65 | string 66 | quoted 67 | single 68 | double 69 | triple 70 | other 71 | unquoted 72 | interpolated 73 | regexp 74 | other 75 | 76 | support 77 | function 78 | class 79 | type 80 | constant 81 | variable 82 | other 83 | 84 | variable 85 | parameter 86 | language 87 | other 88 | 89 | source 90 | 91 | text 92 | 93 | punctuation 94 | definition 95 | section 96 | separator 97 | terminator 98 | """ 99 | 100 | 101 | class NodeList(list): 102 | """ 103 | Methods: 104 | * find(name) 105 | * find_all(name) 106 | * to_completion() 107 | """ 108 | def find(self, name): 109 | for node in self: 110 | if node == name: 111 | return node 112 | return None 113 | 114 | def find_all(self, name): 115 | res = NodeList() 116 | for node in self: 117 | if node == name: 118 | res.append(node) 119 | return res 120 | 121 | def to_completion(self): 122 | # return zip(self, self) 123 | return [(n.name + "\tscope", n.name) for n in self] 124 | 125 | 126 | COMPILED_NODES = NodeList() 127 | COMPILED_HEADS = NodeList() 128 | 129 | 130 | class ScopeNode(object): 131 | """ 132 | Attributes: 133 | * name 134 | * parent 135 | * children 136 | * level | unused 137 | Methods: 138 | * add_child(child) 139 | * tree() 140 | """ 141 | 142 | def __init__(self, name, parent=None, children=None): 143 | self.name = name 144 | self.parent = parent 145 | self.children = children or NodeList() 146 | self.level = parent and parent.level + 1 or 1 147 | 148 | def add_child(self, child): 149 | self.children.append(child) 150 | 151 | def tree(self): 152 | if self.parent: 153 | return self.name + '.' + self.parent.tree() 154 | else: 155 | return self.name 156 | 157 | def __eq__(self, other): 158 | if isinstance(other, basestring): 159 | return str(self) == other 160 | 161 | def __str__(self): 162 | return self.name 163 | 164 | def __repr__(self): 165 | ret = self.name 166 | if self.children: 167 | ret += " {%s}" % ' '.join(repr(child) for child in self.children) 168 | return ret 169 | 170 | 171 | ####################################### 172 | 173 | # parse the DATA string 174 | lines = DATA.split("\n") 175 | 176 | # some variables 177 | indent = " " * 4 178 | indent_level = 0 179 | indents = {} 180 | 181 | # process lines 182 | # Note: expects sane indentation (such as only indent by 1 `indent` at a time) 183 | for line in lines: 184 | if line.isspace() or not len(line): 185 | # skip blank lines 186 | continue 187 | if line.startswith(indent * (indent_level + 1)): 188 | # indent increased 189 | indent_level += 1 190 | if not line.startswith(indent * indent_level): 191 | # indent decreased 192 | for level in range(indent_level - 1, 0, -1): 193 | if line.startswith(indent * level): 194 | indent_level = level 195 | break 196 | 197 | parent = indents[indent_level - 1] if indent_level - 1 in indents else None 198 | node = ScopeNode(line.strip(), parent) 199 | indents[indent_level] = node 200 | 201 | if parent: 202 | parent.add_child(node) 203 | else: 204 | COMPILED_HEADS.append(node) 205 | 206 | COMPILED_NODES.append(node) 207 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [flake8] 2 | max-line-length = 99 3 | # Ignore E402 until https://github.com/PyCQA/pycodestyle/pull/523 is merged 4 | ignore = E201,E221,E222,E241,E402,W503 5 | exclude = ./tinycsscheme/tinycss*,./my_sublime_lib/* 6 | -------------------------------------------------------------------------------- /tinycsscheme/.coveragerc: -------------------------------------------------------------------------------- 1 | [run] 2 | omit = 3 | *tests/* 4 | *tinycss/* 5 | *_ordereddict.py 6 | 7 | 8 | #branch = True 9 | 10 | [report] 11 | # Regexes for lines to exclude from consideration 12 | exclude_lines = 13 | # Have to re-enable the standard pragma 14 | pragma: no cover 15 | 16 | # Don't complain about missing debug-only code: 17 | def __repr__ 18 | -------------------------------------------------------------------------------- /tinycsscheme/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FichteFoll/CSScheme/6575b53d2c40a64839f86e624af33bf86f6b8e34/tinycsscheme/__init__.py -------------------------------------------------------------------------------- /tinycsscheme/css_colors.py: -------------------------------------------------------------------------------- 1 | # http://www.w3schools.com/cssref/css_colornames.asp 2 | 3 | __all__ = ['css_colors'] 4 | 5 | css_colors = { 6 | 'antiquewhite': '#FAEBD7', 7 | 'aqua': '#00FFFF', 8 | 'aquamarine': '#7FFFD4', 9 | 'azure': '#F0FFFF', 10 | 'beige': '#F5F5DC', 11 | 'bisque': '#FFE4C4', 12 | 'black': '#000000', 13 | 'blanchedalmond': '#FFEBCD', 14 | 'blue': '#0000FF', 15 | 'blueviolet': '#8A2BE2', 16 | 'brown': '#A52A2A', 17 | 'burlywood': '#DEB887', 18 | 'cadetblue': '#5F9EA0', 19 | 'chartreuse': '#7FFF00', 20 | 'chocolate': '#D2691E', 21 | 'coral': '#FF7F50', 22 | 'cornflowerblue': '#6495ED', 23 | 'cornsilk': '#FFF8DC', 24 | 'crimson': '#DC143C', 25 | 'cyan': '#00FFFF', 26 | 'darkblue': '#00008B', 27 | 'darkcyan': '#008B8B', 28 | 'darkgoldenrod': '#B8860B', 29 | 'darkgray': '#A9A9A9', 30 | 'darkgreen': '#006400', 31 | 'darkkhaki': '#BDB76B', 32 | 'darkmagenta': '#8B008B', 33 | 'darkolivegreen': '#556B2F', 34 | 'darkorange': '#FF8C00', 35 | 'darkorchid': '#9932CC', 36 | 'darkred': '#8B0000', 37 | 'darksalmon': '#E9967A', 38 | 'darkseagreen': '#8FBC8F', 39 | 'darkslateblue': '#483D8B', 40 | 'darkslategray': '#2F4F4F', 41 | 'darkturquoise': '#00CED1', 42 | 'darkviolet': '#9400D3', 43 | 'deeppink': '#FF1493', 44 | 'deepskyblue': '#00BFFF', 45 | 'dimgray': '#696969', 46 | 'dodgerblue': '#1E90FF', 47 | 'firebrick': '#B22222', 48 | 'floralwhite': '#FFFAF0', 49 | 'forestgreen': '#228B22', 50 | 'fuchsia': '#FF00FF', 51 | 'gainsboro': '#DCDCDC', 52 | 'ghostwhite': '#F8F8FF', 53 | 'gold': '#FFD700', 54 | 'goldenrod': '#DAA520', 55 | 'gray': '#808080', 56 | 'green': '#008000', 57 | 'greenyellow': '#ADFF2F', 58 | 'honeydew': '#F0FFF0', 59 | 'hotpink': '#FF69B4', 60 | 'indianred': '#CD5C5C', 61 | 'indigo': '#4B0082', 62 | 'ivory': '#FFFFF0', 63 | 'khaki': '#F0E68C', 64 | 'lavender': '#E6E6FA', 65 | 'lavenderblush': '#FFF0F5', 66 | 'lawngreen': '#7CFC00', 67 | 'lemonchiffon': '#FFFACD', 68 | 'lightblue': '#ADD8E6', 69 | 'lightcoral': '#F08080', 70 | 'lightcyan': '#E0FFFF', 71 | 'lightgoldenrodyellow': '#FAFAD2', 72 | 'lightgray': '#D3D3D3', 73 | 'lightgreen': '#90EE90', 74 | 'lightpink': '#FFB6C1', 75 | 'lightsalmon': '#FFA07A', 76 | 'lightseagreen': '#20B2AA', 77 | 'lightskyblue': '#87CEFA', 78 | 'lightslategray': '#778899', 79 | 'lightsteelblue': '#B0C4DE', 80 | 'lightyellow': '#FFFFE0', 81 | 'lime': '#00FF00', 82 | 'limegreen': '#32CD32', 83 | 'linen': '#FAF0E6', 84 | 'magenta': '#FF00FF', 85 | 'maroon': '#800000', 86 | 'mediumaquamarine': '#66CDAA', 87 | 'mediumblue': '#0000CD', 88 | 'mediumorchid': '#BA55D3', 89 | 'mediumpurple': '#9370DB', 90 | 'mediumseagreen': '#3CB371', 91 | 'mediumslateblue': '#7B68EE', 92 | 'mediumspringgreen': '#00FA9A', 93 | 'mediumturquoise': '#48D1CC', 94 | 'mediumvioletred': '#C71585', 95 | 'midnightblue': '#191970', 96 | 'mintcream': '#F5FFFA', 97 | 'mistyrose': '#FFE4E1', 98 | 'moccasin': '#FFE4B5', 99 | 'navajowhite': '#FFDEAD', 100 | 'navy': '#000080', 101 | 'oldlace': '#FDF5E6', 102 | 'olive': '#808000', 103 | 'olivedrab': '#6B8E23', 104 | 'orange': '#FFA500', 105 | 'orangered': '#FF4500', 106 | 'orchid': '#DA70D6', 107 | 'palegoldenrod': '#EEE8AA', 108 | 'palegreen': '#98FB98', 109 | 'paleturquoise': '#AFEEEE', 110 | 'palevioletred': '#DB7093', 111 | 'papayawhip': '#FFEFD5', 112 | 'peachpuff': '#FFDAB9', 113 | 'peru': '#CD853F', 114 | 'pink': '#FFC0CB', 115 | 'plum': '#DDA0DD', 116 | 'powderblue': '#B0E0E6', 117 | 'purple': '#800080', 118 | 'red': '#FF0000', 119 | 'rosybrown': '#BC8F8F', 120 | 'royalblue': '#4169E1', 121 | 'saddlebrown': '#8B4513', 122 | 'salmon': '#FA8072', 123 | 'sandybrown': '#F4A460', 124 | 'seagreen': '#2E8B57', 125 | 'seashell': '#FFF5EE', 126 | 'sienna': '#A0522D', 127 | 'silver': '#C0C0C0', 128 | 'skyblue': '#87CEEB', 129 | 'slateblue': '#6A5ACD', 130 | 'slategray': '#708090', 131 | 'snow': '#FFFAFA', 132 | 'springgreen': '#00FF7F', 133 | 'steelblue': '#4682B4', 134 | 'tan': '#D2B48C', 135 | 'teal': '#008080', 136 | 'thistle': '#D8BFD8', 137 | 'tomato': '#FF6347', 138 | 'turquoise': '#40E0D0', 139 | 'violet': '#EE82EE', 140 | 'wheat': '#F5DEB3', 141 | 'white': '#FFFFFF', 142 | 'whitesmoke': '#F5F5F5', 143 | 'yellow': '#FFFF00', 144 | 'yellowgreen': '#9ACD32' 145 | } 146 | -------------------------------------------------------------------------------- /tinycsscheme/parser.py: -------------------------------------------------------------------------------- 1 | """Parse CSS-like format optimized for use with Text Mate and Sublime Text Color Schemes. 2 | 3 | Extends tinycss's css21 basic parser and differs in the following points: 4 | 5 | - Generally, all at-rules are only allowed once in a scope, only accept a single value as their 6 | head, no body. Must be STRING, IDENT, HASH or a valid uuid4. Examples: 7 | 8 | @some-at-rule "a string value"; 9 | @uuid 2e3af29f-ebee-431f-af96-72bda5d4c144; 10 | 11 | - At-rules are allowed in rulesets. 12 | 13 | - Declarations may only provide a list (separated by spaces) of values of the type FUNCTION, 14 | HASH, STRING, IDENT, INTEGER (and DELIM commas for function parameters). 15 | """ 16 | 17 | 18 | __all__ = ( 19 | # from tinycss.css21 imported 20 | 'ParseError', 21 | # from this file 22 | 'parse_stylesheet', 23 | 'StringRule', 24 | 'CSSchemeParser', 25 | ) 26 | 27 | 28 | import re 29 | from itertools import chain 30 | 31 | from .tinycss.css21 import (ParseError, Declaration, RuleSet, CSS21Parser, 32 | strip_whitespace, validate_any) 33 | 34 | 35 | def is_uuid(test): 36 | return bool(re.match(r"[a-f0-9]{8}-[a-f0-9]{4}-4[a-f0-9]{3}-[89ab][a-f0-9]{3}-[a-f0-9]{12}", 37 | test, re.I)) 38 | 39 | 40 | def strvalue(token): 41 | """Get the string value of a token.""" 42 | if token.type == 'DIMENSION': 43 | return token.as_css() 44 | else: 45 | return str(token.value) 46 | 47 | 48 | def parse_stylesheet(css_unicode, encoding=None): 49 | """Shorthand to parse a css stylesheet file.""" 50 | return CSSchemeParser().parse_stylesheet(css_unicode, encoding) 51 | 52 | 53 | class StringRule(object): 54 | """Any parsed rule with a single STRING head (e.g. @comment). 55 | 56 | .. attribute:: at_keyword 57 | 58 | The at-keyword for this rule. 59 | 60 | .. attribute:: value 61 | 62 | The value for this rule (a Token). 63 | """ 64 | at_keyword = '' 65 | 66 | def __init__(self, keyword, value, line, column): 67 | self.at_keyword = keyword 68 | self.value = value 69 | self.line = line 70 | self.column = column 71 | 72 | def __repr__(self): 73 | return ('<{0.__class__.__name__} {0.at_keyword} {0.line}:{0.column} ' 74 | '{0.value}>'.format(self)) 75 | 76 | 77 | class CSSchemeParser(CSS21Parser): 78 | """Documentation to be here. 79 | """ 80 | 81 | def _check_at_rule_occurences(self, rule, previous_rules): 82 | for previous_rule in previous_rules: 83 | if previous_rule.at_keyword == rule.at_keyword: 84 | raise ParseError(rule, 85 | '{0} only allowed once, previously line {1}' 86 | .format(rule.at_keyword, previous_rule.line)) 87 | 88 | def parse_at_rule(self, rule, previous_rules, errors, context): 89 | """Parse an at-rule. 90 | 91 | This method handles @uuid, @name and any other "string rule". 92 | Example: 93 | @author "I am not an author"; 94 | @name I-Myself; 95 | """ 96 | # Every at-rule is only supposed to be used once in a context 97 | self._check_at_rule_occurences(rule, previous_rules) 98 | 99 | # Allow @uuid only in root 100 | if context != 'stylesheet' and rule.at_keyword == "@uuid": 101 | raise ParseError(rule, '{0} not allowed in {1}'.format(rule.at_keyword, context)) 102 | 103 | # Check format: 104 | # - No body 105 | # - Only allow exactly one token in head (obviously) 106 | head = rule.head 107 | if rule.body is not None: 108 | raise ParseError(rule.head[-1] if rule.head else rule, "expected ';', got a block") 109 | 110 | if not head: 111 | raise ParseError(rule, 'expected value for {0} rule'.format(rule.at_keyword)) 112 | if len(head) > 1: 113 | raise ParseError(head[1], 'expected 1 token for {0} rule, got {1}' 114 | .format(rule.at_keyword, len(head))) 115 | token = head[0] 116 | 117 | # DIMENSION is used for uuids that start with a number 118 | whole_value = strvalue(token) 119 | if not (token.type in ('STRING', 'IDENT', 'HASH') 120 | or (token.type == 'DIMENSION' and is_uuid(whole_value))): 121 | raise ParseError(rule, 'expected STRING, IDENT or HASH token or a valid uuid4 for ' 122 | '{0} rule, got {1}'.format(rule.at_keyword, token.type)) 123 | 124 | return StringRule(rule.at_keyword, token, rule.line, rule.column) 125 | 126 | def parse_ruleset(self, first_token, tokens): 127 | """Parse a ruleset: a selector followed by declaration block. 128 | 129 | Modified in that we call :meth:`parse_declarations_and_at_rules` instead of 130 | :meth:`parse_declaration_list` and manually add at-rules afterwards. 131 | """ 132 | selector = [] 133 | for token in chain([first_token], tokens): 134 | if token.type == '{': 135 | # Parse/validate once we've read the whole rule 136 | selector = strip_whitespace(selector) 137 | if not selector: 138 | raise ParseError(first_token, 'empty selector') 139 | for selector_token in selector: 140 | validate_any(selector_token, 'selector') 141 | 142 | declarations, at_rules, errors = \ 143 | self.parse_declarations_and_at_rules(token.content, 'ruleset') 144 | 145 | ruleset = RuleSet(selector, declarations, first_token.line, first_token.column) 146 | # Set at-rules manually (because I cba to create yet another class for that) 147 | ruleset.at_rules = at_rules 148 | 149 | return ruleset, errors 150 | else: 151 | selector.append(token) 152 | raise ParseError(token, 'no declaration block found for ruleset') 153 | 154 | def parse_declarations_and_at_rules(self, tokens, context): 155 | """Allow each declaration only once. 156 | """ 157 | declarations, at_rules, errors = \ 158 | super(CSSchemeParser, self).parse_declarations_and_at_rules(tokens, context) 159 | 160 | known = set() 161 | for d in declarations: 162 | if d.name in known: 163 | errors.append(ParseError(d, "property {0} only allowed once".format(d.name))) 164 | declarations.remove(d) 165 | else: 166 | known.add(d.name) 167 | return declarations, at_rules, errors 168 | 169 | def parse_declaration(self, tokens): 170 | """Parse a single declaration. 171 | 172 | :returns: 173 | a :class:`Declaration` 174 | :raises: 175 | :class:`~.parsing.ParseError` if the tokens do not match the 176 | 'declaration' production of the core grammar. 177 | """ 178 | tokens = iter(tokens) 179 | 180 | name_token = next(tokens) # Assume there is at least one 181 | if name_token.type == 'IDENT': 182 | # tmThemes are case-sensitive 183 | property_name = name_token.value 184 | else: 185 | raise ParseError(name_token, 186 | 'expected a property name, got {0}'.format(name_token.type)) 187 | 188 | # Proceed with value 189 | for token in tokens: 190 | if token.type == ':': 191 | break 192 | elif token.type != 'S': 193 | raise ParseError( 194 | token, "expected ':', got {0}".format(token.type)) 195 | else: 196 | raise ParseError(name_token, "expected ':'") 197 | 198 | value = strip_whitespace(list(tokens)) 199 | if not value: 200 | raise ParseError(name_token, 201 | "expected a property value for property {0}".format(property_name)) 202 | 203 | # Only allow a list of HASH, IDENT, STRING, FUNCTION (and S) (minimal requirements). 204 | # STRING is for arbitrary properties (since all scheme values are strings). 205 | # IDENT and INTEGER are technically only short form and accepted for convenience. 206 | # Inside FUNCTIONS we also allow: DELIM, INTEGER, NUMBER and PERCENTAGE. 207 | def check_token_types(tokens, fn=None): 208 | for token in tokens: 209 | if not (token.type in ('S', 'IDENT', 'STRING', 'HASH', 'FUNCTION', 'INTEGER') or 210 | fn and token.type in ('DELIM', 'INTEGER', 'NUMBER', 'PERCENTAGE')): 211 | match_type = token.type in ('}', ')', ']') and 'unmatched' or 'unexpected' 212 | raise ParseError(token, '{0} {1} token for property {2}{3}' 213 | .format(match_type, token.type, property_name, 214 | " in function '%s()'" % fn if fn else '')) 215 | if token.type == 'FUNCTION': 216 | check_token_types(token.content, token.function_name) 217 | # elif token.is_container: 218 | # check_token_types(token.content) 219 | 220 | check_token_types(value) 221 | 222 | # Note: '!important' priority ignored 223 | return Declaration(property_name, value, None, name_token.line, name_token.column) 224 | -------------------------------------------------------------------------------- /tinycsscheme/test_coverage.bat: -------------------------------------------------------------------------------- 1 | py.test --cov-config .coveragerc ^ 2 | --cov . ^ 3 | --cov-report html ^ 4 | tests/ 5 | -------------------------------------------------------------------------------- /tinycsscheme/tests/__init__.py: -------------------------------------------------------------------------------- 1 | # coding: utf8 2 | """ 3 | Test suite for tinycsscheme 4 | --------------------------- 5 | """ 6 | 7 | 8 | from __future__ import unicode_literals 9 | 10 | 11 | # from ...tinycss.tests import assert_errors 12 | def assert_errors(errors, expected_errors): 13 | """Test not complete error messages but only substrings.""" 14 | assert len(errors) == len(expected_errors) 15 | for error, expected in zip(errors, expected_errors): 16 | assert expected in str(error) 17 | 18 | 19 | # from ...tinycss.tests.test_tokenizer 20 | def jsonify(tokens): 21 | """Turn tokens into "JSON-compatible" data structures.""" 22 | for token in tokens: 23 | if token.type == 'FUNCTION': 24 | yield (token.type, token.function_name, 25 | list(jsonify(token.content))) 26 | elif token.is_container: 27 | yield token.type, list(jsonify(token.content)) 28 | else: 29 | yield token.type, token.value 30 | 31 | 32 | def tuplify(rule): 33 | if rule.at_keyword: 34 | return (rule.at_keyword, list(jsonify([rule.value]))) 35 | else: 36 | return (rule.selector.as_css(), 37 | [(decl.name, list(jsonify(decl.value))) 38 | for decl in rule.declarations], 39 | [tuplify(at_rule) 40 | for at_rule in rule.at_rules]) 41 | -------------------------------------------------------------------------------- /tinycsscheme/tests/css_test.csscheme: -------------------------------------------------------------------------------- 1 | /* @settings is a special at-rule that expects a body with the allowed meta 2 | * settings (like background and foreground color). 3 | * 4 | * All other @rules are arbitrary as long as only an identifier or a string 5 | * follows, e.g. name and uuid or "foldingStartMatches" or whatever, if you need 6 | * that. They will be added as an arbitrary dict entry with a string value. 7 | */ 8 | 9 | @name "Test Scheme"; /* a string */ 10 | 11 | @uuid pls-no; /* an ident */ 12 | 13 | * { 14 | background: #111111; 15 | foreground: #888888:; 16 | lineHighlight: #12345678; 17 | } 18 | 19 | 20 | /* no name (must be supported) */ 21 | source { 22 | foreground: #FF00002; 23 | fontStyle: bold italicc; 24 | @random-at-rule yeah; 25 | } 26 | 27 | 28 | /* name in at-rule (this is already supported, see above) */ 29 | text { 30 | @name "Text"; 31 | foreground: #00FF00; 32 | fontStyle: bold italic; 33 | } 34 | -------------------------------------------------------------------------------- /tinycsscheme/tests/scss_test.scsscheme: -------------------------------------------------------------------------------- 1 | /* `*` is a special selector that expects a body with the general settings (like 2 | * background and foreground color, indent guides ...). 3 | * 4 | * All other @rules are arbitrary as long as only an identifier or a string 5 | * follows, e.g. name and uuid or "foldingStartMatches" or whatever, if you need 6 | * that. They will be added as an arbitrary dict entry with a string value. 7 | */ 8 | 9 | // to be commented out 10 | //@import "yeah", 'single', url(it werks); 11 | 12 | // line comment 13 | $fore: #888888; 14 | $boolean-list: true, false, null; 15 | $i: 1; 16 | $i: 2 !default; 17 | 18 | @debug $i; 19 | 20 | @name "Test Scheme"; /* a string */ 21 | 22 | @uuid 16ebecdd-4dc2-4d5b-8847-0eb6c8c47f3c; /* a uuid */ 23 | 24 | 25 | @function grid-width($n) { 26 | $z: 123; // makes no sense 27 | @return $n * 2 + ($n - 1) * $z; 28 | } 29 | 30 | @mixin red($background: true) { 31 | foreground: red; 32 | @if $background { 33 | background: #F00; 34 | } 35 | } 36 | 37 | @mixin bolded-red($args...) { 38 | font-weight: bold; 39 | @include red($args...); 40 | } 41 | 42 | * { 43 | @if 1 + 1 == $i { border: solid; } 44 | @else if $i < 3 { border: dotted; } 45 | @else { border: double; } 46 | 47 | background: #111111; 48 | // inline commend 49 | foreground: /**/ $fore 50 | // here as well 51 | ; 52 | // will generate a string because SASS doesn't like literal hashes with length 8 53 | lineHighlight: '#12345678'; 54 | // will generate a hash but is unnecessarily verbose 55 | caret: unquote('#12345678') // no final ; 56 | } 57 | 58 | source { 59 | foreground: darken($fore, 20/$i - 2+2); // #555555 60 | fontStyle: bold italic; 61 | @random-at-rule yeah; // an ident 62 | } 63 | 64 | /* optional name in at-rule */ 65 | text%default { 66 | @name "Text"; 67 | @comment "nothing of \n interest"; 68 | @interpolation "string with #{$boolean-list} interpolation"; 69 | foreground: #00FF00; 70 | fontStyle: bold italic; 71 | } 72 | 73 | 74 | string { 75 | @extend %default !optional; 76 | foreground: rgba(0,0,0,/**/0.2); 77 | some-color: hsl($hue: 150, $saturation: 100%, $lightness: 50%); 78 | width: "#{grid-width((1 + 2) * 3)}"; // gotta be string 79 | 80 | /* nested */ 81 | & constant { 82 | foreground: darken($fore, 70); 83 | } 84 | 85 | red { 86 | @include bolded-red($background: false); 87 | } 88 | 89 | &, &.nope { 90 | @include red(true); 91 | } 92 | } 93 | 94 | /* interpolation in selector */ 95 | String and Number #{hsl(0, 30%, 20%)} string, number { 96 | foreground: #0000FF; 97 | fontStyle: /**/underline ; 98 | } 99 | 100 | @for $i from 1 through 3 { 101 | item-1-#{$i} { width: "#{2 * $i}"; } // gotta be string 102 | } 103 | 104 | @each $animal in puma, sea-slug, egret, salamander { 105 | #{$animal}-icon { 106 | background-image: "/images/#{$animal}.png"; 107 | } 108 | } 109 | 110 | 111 | @while $i < 4 { 112 | item-2-#{$i} { height: "#{4 * $i}"; } // gotta be string 113 | $i: $i + 1; 114 | } -------------------------------------------------------------------------------- /tinycsscheme/tests/test_parser.py: -------------------------------------------------------------------------------- 1 | """ 2 | Tests for the CSScheme parser 3 | ---------------------------- 4 | Based on the original tests for tinycss's CSS 2.1 parser 5 | which is (c) 2012 by Simon Sapin and BSD-licensed. 6 | """ 7 | 8 | import pytest 9 | 10 | from ..parser import CSSchemeParser 11 | from ..tinycss.css21 import CSS21Parser 12 | from . import jsonify, assert_errors, tuplify 13 | 14 | 15 | # Carried over from css21 (to ensure that basic stuff still works) 16 | @pytest.mark.parametrize(('css_source', 'expected_rules', 'expected_errors'), [ 17 | (' /* hey */\n', [], []), 18 | 19 | ('foo{} /* hey */\n@bar;@baz{}', 20 | [('foo', []), ('@bar', [], None), ('@baz', [], [])], []), 21 | 22 | ('@import "foo.css"/**/;', [ 23 | ('@import', [('STRING', 'foo.css')], None)], []), 24 | 25 | ('@import "foo.css"/**/', [ 26 | ('@import', [('STRING', 'foo.css')], None)], []), 27 | 28 | ('@import "foo.css', [ 29 | ('@import', [('STRING', 'foo.css')], None)], []), 30 | 31 | ('{}', [], ['empty selector']), 32 | 33 | ('a{b:4}', [('a', [('b', [('INTEGER', 4)])])], []), 34 | 35 | ('@page {\t b: 4; @margin}', [('@page', [], [ 36 | ('S', '\t '), ('IDENT', 'b'), (':', ':'), ('S', ' '), ('INTEGER', 4), 37 | (';', ';'), ('S', ' '), ('ATKEYWORD', '@margin'), 38 | ])], []), 39 | 40 | ('foo', [], ['no declaration block found']), 41 | 42 | ('foo @page {} bar {}', [('bar', [])], 43 | ['unexpected ATKEYWORD token in selector']), 44 | 45 | ('foo { content: "unclosed string;\n color:red; ; margin/**/\n: 2cm; }', 46 | [('foo', [('margin', [('DIMENSION', 2)])])], 47 | ['unexpected BAD_STRING token in property value']), 48 | 49 | ('foo { 4px; bar: 12% }', 50 | [('foo', [('bar', [('PERCENTAGE', 12)])])], 51 | ['expected a property name, got DIMENSION']), 52 | 53 | ('foo { bar! 3cm auto ; baz: 7px }', 54 | [('foo', [('baz', [('DIMENSION', 7)])])], 55 | ["expected ':', got DELIM"]), 56 | 57 | ('foo { bar ; baz: {("}"/* comment */) {0@fizz}} }', 58 | [('foo', [('baz', [('{', [ 59 | ('(', [('STRING', '}')]), ('S', ' '), 60 | ('{', [('INTEGER', 0), ('ATKEYWORD', '@fizz')]) 61 | ])])])], 62 | ["expected ':'"]), 63 | 64 | ('foo { bar: ; baz: not(z) }', 65 | [('foo', [('baz', [('FUNCTION', 'not', [('IDENT', 'z')])])])], 66 | ['expected a property value']), 67 | 68 | ('foo { bar: (]) ; baz: U+20 }', 69 | [('foo', [('baz', [('UNICODE-RANGE', 'U+20')])])], 70 | ['unmatched ] token in (']), 71 | ]) 72 | def test_core_parser(css_source, expected_rules, expected_errors): 73 | class CoreParser(CSSchemeParser): 74 | """A parser that always accepts unparsed at-rules and is reduced to 75 | the core functions. 76 | """ 77 | def parse_at_rule(self, rule, stylesheet_rules, errors, context): 78 | return rule 79 | 80 | # parse_ruleset = CSS21Parser.parse_ruleset 81 | parse_declaration = CSS21Parser.parse_declaration 82 | 83 | stylesheet = CoreParser().parse_stylesheet(css_source) 84 | assert_errors(stylesheet.errors, expected_errors) 85 | result = [ 86 | (rule.at_keyword, list(jsonify(rule.head)), 87 | list(jsonify(rule.body)) 88 | if rule.body is not None else None) 89 | if rule.at_keyword else 90 | (rule.selector.as_css(), [ 91 | (decl.name, list(jsonify(decl.value))) 92 | for decl in rule.declarations]) 93 | for rule in stylesheet.rules 94 | ] 95 | assert result == expected_rules 96 | 97 | 98 | @pytest.mark.parametrize(('css_source', 'expected_rules', 'expected_errors'), [ 99 | ('@charset "ascii"; foo{}', 2, []), 100 | (' @charset "ascii"; foo { } ', 2, []), 101 | ('@charset ascii;', 1, []), 102 | ('@charset #123456;', 1, []), 103 | ('@uuid 2e3af29f-ebee-431f-af96-72bda5d4c144;', 1, []), 104 | ('@uuid 02019C6E-C747-44D5-94B5-110B867C1C22;', 1, []), # starts with 0 105 | # Errors 106 | ('foo{} @lipsum{} bar{}', 2, 107 | ["expected ';', got a block"]), 108 | ('@lipsum;', 0, 109 | ["expected value for @lipsum rule"]), 110 | ('@lipsum a b;', 0, 111 | ["expected 1 token for @lipsum rule, got 3"]), 112 | ('@lipsum 23;', 0, 113 | ["expected STRING, IDENT or HASH token or a valid uuid4 for @lipsum " 114 | "rule, got INTEGER"]), 115 | ('foo {@uuid #122323;}', 1, 116 | ["@uuid not allowed in ruleset"]), 117 | ('@baz ascii; @baz asciii;', 1, 118 | ["@baz only allowed once, previously line 1"]), 119 | # -vvv- not hexadecimal 120 | ('@uuid 2e3af29f-ebee-431f-af96-72bda5d4cxyz;', 0, 121 | ["expected STRING, IDENT or HASH token or a valid uuid4 for @uuid rule, " 122 | "got DIMENSION"]), 123 | # -v- must be 4 124 | ('@uuid 2e3af29f-ebee-331f-af96-72bda5d4c144;', 0, 125 | ["expected STRING, IDENT or HASH token or a valid uuid4 for @uuid rule, " 126 | "got DIMENSION"]), 127 | ]) 128 | def test_at_rules(css_source, expected_rules, expected_errors): 129 | stylesheet = CSSchemeParser().parse_stylesheet(css_source) 130 | assert_errors(stylesheet.errors, expected_errors) 131 | assert len(stylesheet.rules) == expected_rules 132 | 133 | 134 | @pytest.mark.parametrize(('css_source', 'expected_rules', 'expected_errors'), [ 135 | ('foo {/* hey */}\n', 136 | [('foo', [], [])], 137 | []), 138 | 139 | (' * {}', 140 | [('*', [], [])], 141 | []), 142 | 143 | ('foo {@name "ascii"} foo{}', 144 | [('foo', [], [('@name', [('STRING', "ascii")])]), 145 | ('foo', [], [])], 146 | []), 147 | 148 | ('foo {decl: "im-a string"} foo{decl: #123456; decl2: ident}', 149 | [('foo', 150 | [('decl', [('STRING', "im-a string")])], 151 | []), 152 | ('foo', 153 | [('decl', [('HASH', "#123456")]), 154 | ('decl2', [('IDENT', "ident")])], 155 | [])], 156 | []), 157 | 158 | ('fooz {decl: function(param1, param2)}', 159 | [('fooz', 160 | [('decl', [('FUNCTION', "function", 161 | [('IDENT', "param1"), 162 | ('DELIM', ","), 163 | ('S', " "), 164 | ('IDENT', "param2")])])], 165 | [])], 166 | []), 167 | 168 | ('fooz {decl: function(0, 1% 0.2)}', 169 | [('fooz', 170 | [('decl', [('FUNCTION', "function", 171 | [('INTEGER', 0), 172 | ('DELIM', ","), 173 | ('S', " "), 174 | ('PERCENTAGE', 1), 175 | ('S', " "), 176 | ('NUMBER', 0.2)])])], 177 | [])], 178 | []), 179 | 180 | ('foo {list: mixed ident and "string list" and 1;}', 181 | [('foo', 182 | [('list', [('IDENT', "mixed"), 183 | ('S', " "), 184 | ('IDENT', "ident"), 185 | ('S', " "), 186 | ('IDENT', "and"), 187 | ('S', " "), 188 | ('STRING', "string list"), 189 | ('S', " "), 190 | ('IDENT', "and"), 191 | ('S', " "), 192 | ('INTEGER', 1)])], 193 | [])], 194 | []), 195 | 196 | 197 | # Errors 198 | ('foo {decl: 1.2; decl2: "str":; decl3: some ]}', 199 | [('foo', [], [])], 200 | ["unexpected NUMBER token for property decl", 201 | "unexpected : token for property decl2", 202 | "unmatched ] token for property decl3"]), 203 | 204 | ('foo {decl: a; decl: b}', 205 | [('foo', [('decl', [('IDENT', "a")])], [])], 206 | ["property decl only allowed once"]), 207 | 208 | ('foo {"decl": a; decl2 a; decl3: ;}', 209 | [('foo', [], [])], 210 | ["expected a property name, got STRING", 211 | "expected ':', got IDENT", 212 | "expected a property value for property decl3"]), 213 | 214 | ('foo {decl ;}', 215 | [('foo', [], [])], 216 | ["expected ':'"]), 217 | 218 | ('fooz {decl: function(param1}', 219 | [('fooz', 220 | [], 221 | [])], 222 | ["unmatched } token for property decl in function 'function()'"]), 223 | ]) 224 | def test_rulesets(css_source, expected_rules, expected_errors): 225 | stylesheet = CSSchemeParser().parse_stylesheet(css_source) 226 | assert_errors(stylesheet.errors, expected_errors) 227 | result = [tuplify(rule) for rule in stylesheet.rules] 228 | assert result == expected_rules 229 | -------------------------------------------------------------------------------- /tinycsscheme/tinycss/LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2012 by Simon Sapin. 2 | 3 | Some rights reserved. 4 | 5 | Redistribution and use in source and binary forms, with or without 6 | modification, are permitted provided that the following conditions are 7 | met: 8 | 9 | * Redistributions of source code must retain the above copyright 10 | notice, this list of conditions and the following disclaimer. 11 | 12 | * Redistributions in binary form must reproduce the above 13 | copyright notice, this list of conditions and the following 14 | disclaimer in the documentation and/or other materials provided 15 | with the distribution. 16 | 17 | * The names of the contributors may not be used to endorse or 18 | promote products derived from this software without specific 19 | prior written permission. 20 | 21 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 22 | "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 23 | LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR 24 | A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT 25 | OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 26 | SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT 27 | LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 28 | DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY 29 | THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 30 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 31 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 32 | -------------------------------------------------------------------------------- /tinycsscheme/tinycss/__init__.py: -------------------------------------------------------------------------------- 1 | # coding: utf8 2 | """ 3 | tinycss 4 | ------- 5 | 6 | A CSS parser, and nothing else. 7 | 8 | :copyright: (c) 2012 by Simon Sapin. 9 | :license: BSD, see LICENSE for more details. 10 | """ 11 | 12 | import sys 13 | 14 | from .version import VERSION 15 | __version__ = VERSION 16 | 17 | from .css21 import CSS21Parser 18 | from .page3 import CSSPage3Parser 19 | 20 | 21 | PARSER_MODULES = { 22 | 'page3': CSSPage3Parser, 23 | } 24 | 25 | 26 | def make_parser(*features, **kwargs): 27 | """Make a parser object with the chosen features. 28 | 29 | :param features: 30 | Positional arguments are base classes the new parser class will extend. 31 | The string ``'page3'`` is accepted as short for 32 | :class:`~page3.CSSPage3Parser`. 33 | :param kwargs: 34 | Keyword arguments are passed to the parser’s constructor. 35 | :returns: 36 | An instance of a new subclass of :class:`CSS21Parser` 37 | 38 | """ 39 | if features: 40 | bases = tuple(PARSER_MODULES.get(f, f) for f in features) 41 | parser_class = type('CustomCSSParser', bases + (CSS21Parser,), {}) 42 | else: 43 | parser_class = CSS21Parser 44 | return parser_class(**kwargs) 45 | -------------------------------------------------------------------------------- /tinycsscheme/tinycss/page3.py: -------------------------------------------------------------------------------- 1 | # coding: utf8 2 | """ 3 | tinycss.page3 4 | ------------------ 5 | 6 | Support for CSS 3 Paged Media syntax: 7 | http://dev.w3.org/csswg/css3-page/ 8 | 9 | Adds support for named page selectors and margin rules. 10 | 11 | :copyright: (c) 2012 by Simon Sapin. 12 | :license: BSD, see LICENSE for more details. 13 | """ 14 | 15 | from __future__ import unicode_literals, division 16 | from .css21 import CSS21Parser, ParseError 17 | 18 | 19 | class MarginRule(object): 20 | """A parsed at-rule for margin box. 21 | 22 | .. attribute:: at_keyword 23 | 24 | One of the 16 following strings: 25 | 26 | * ``@top-left-corner`` 27 | * ``@top-left`` 28 | * ``@top-center`` 29 | * ``@top-right`` 30 | * ``@top-right-corner`` 31 | * ``@bottom-left-corner`` 32 | * ``@bottom-left`` 33 | * ``@bottom-center`` 34 | * ``@bottom-right`` 35 | * ``@bottom-right-corner`` 36 | * ``@left-top`` 37 | * ``@left-middle`` 38 | * ``@left-bottom`` 39 | * ``@right-top`` 40 | * ``@right-middle`` 41 | * ``@right-bottom`` 42 | 43 | .. attribute:: declarations 44 | 45 | A list of :class:`~.css21.Declaration` objects. 46 | 47 | .. attribute:: line 48 | 49 | Source line where this was read. 50 | 51 | .. attribute:: column 52 | 53 | Source column where this was read. 54 | 55 | """ 56 | 57 | def __init__(self, at_keyword, declarations, line, column): 58 | self.at_keyword = at_keyword 59 | self.declarations = declarations 60 | self.line = line 61 | self.column = column 62 | 63 | 64 | class CSSPage3Parser(CSS21Parser): 65 | """Extend :class:`~.css21.CSS21Parser` for `CSS 3 Paged Media`_ syntax. 66 | 67 | .. _CSS 3 Paged Media: http://dev.w3.org/csswg/css3-page/ 68 | 69 | Compared to CSS 2.1, the ``at_rules`` and ``selector`` attributes of 70 | :class:`~.css21.PageRule` objects are modified: 71 | 72 | * ``at_rules`` is not always empty, it is a list of :class:`MarginRule` 73 | objects. 74 | 75 | * ``selector``, instead of a single string, is a tuple of the page name 76 | and the pseudo class. Each of these may be a ``None`` or a string. 77 | 78 | +--------------------------+------------------------+ 79 | | CSS | Parsed selectors | 80 | +==========================+========================+ 81 | | .. code-block:: css | .. code-block:: python | 82 | | | | 83 | | @page {} | (None, None) | 84 | | @page :first {} | (None, 'first') | 85 | | @page chapter {} | ('chapter', None) | 86 | | @page table:right {} | ('table', 'right') | 87 | +--------------------------+------------------------+ 88 | 89 | """ 90 | 91 | PAGE_MARGIN_AT_KEYWORDS = [ 92 | '@top-left-corner', 93 | '@top-left', 94 | '@top-center', 95 | '@top-right', 96 | '@top-right-corner', 97 | '@bottom-left-corner', 98 | '@bottom-left', 99 | '@bottom-center', 100 | '@bottom-right', 101 | '@bottom-right-corner', 102 | '@left-top', 103 | '@left-middle', 104 | '@left-bottom', 105 | '@right-top', 106 | '@right-middle', 107 | '@right-bottom', 108 | ] 109 | 110 | def parse_at_rule(self, rule, previous_rules, errors, context): 111 | if rule.at_keyword in self.PAGE_MARGIN_AT_KEYWORDS: 112 | if context != '@page': 113 | raise ParseError(rule, 114 | '%s rule not allowed in %s' % (rule.at_keyword, context)) 115 | if rule.head: 116 | raise ParseError(rule.head[0], 117 | 'unexpected %s token in %s rule header' 118 | % (rule.head[0].type, rule.at_keyword)) 119 | declarations, body_errors = self.parse_declaration_list(rule.body) 120 | errors.extend(body_errors) 121 | return MarginRule(rule.at_keyword, declarations, 122 | rule.line, rule.column) 123 | return super(CSSPage3Parser, self).parse_at_rule( 124 | rule, previous_rules, errors, context) 125 | 126 | def parse_page_selector(self, head): 127 | """Parse an @page selector. 128 | 129 | :param head: 130 | The ``head`` attribute of an unparsed :class:`AtRule`. 131 | :returns: 132 | A page selector. For CSS 2.1, this is 'first', 'left', 'right' 133 | or None. 'blank' is added by GCPM. 134 | :raises: 135 | :class`~parsing.ParseError` on invalid selectors 136 | 137 | """ 138 | if not head: 139 | return (None, None), (0, 0, 0) 140 | if head[0].type == 'IDENT': 141 | name = head.pop(0).value 142 | while head and head[0].type == 'S': 143 | head.pop(0) 144 | if not head: 145 | return (name, None), (1, 0, 0) 146 | name_specificity = (1,) 147 | else: 148 | name = None 149 | name_specificity = (0,) 150 | if (len(head) == 2 and head[0].type == ':' 151 | and head[1].type == 'IDENT'): 152 | pseudo_class = head[1].value 153 | specificity = { 154 | 'first': (1, 0), 'blank': (1, 0), 155 | 'left': (0, 1), 'right': (0, 1), 156 | }.get(pseudo_class) 157 | if specificity: 158 | return (name, pseudo_class), (name_specificity + specificity) 159 | raise ParseError(head[0], 'invalid @page selector') 160 | -------------------------------------------------------------------------------- /tinycsscheme/tinycss/parsing.py: -------------------------------------------------------------------------------- 1 | # coding: utf8 2 | """ 3 | tinycss.parsing 4 | --------------- 5 | 6 | Utilities for parsing lists of tokens. 7 | 8 | :copyright: (c) 2012 by Simon Sapin. 9 | :license: BSD, see LICENSE for more details. 10 | """ 11 | 12 | from __future__ import unicode_literals 13 | 14 | 15 | # TODO: unit tests 16 | 17 | def split_on_comma(tokens): 18 | """Split a list of tokens on commas, ie ``,`` DELIM tokens. 19 | 20 | Only "top-level" comma tokens are splitting points, not commas inside a 21 | function or other :class:`ContainerToken`. 22 | 23 | :param tokens: 24 | An iterable of :class:`~.token_data.Token` or 25 | :class:`~.token_data.ContainerToken`. 26 | :returns: 27 | A list of lists of tokens 28 | 29 | """ 30 | parts = [] 31 | this_part = [] 32 | for token in tokens: 33 | if token.type == 'DELIM' and token.value == ',': 34 | parts.append(this_part) 35 | this_part = [] 36 | else: 37 | this_part.append(token) 38 | parts.append(this_part) 39 | return parts 40 | 41 | 42 | def strip_whitespace(tokens): 43 | """Remove whitespace at the beggining and end of a token list. 44 | 45 | Whitespace tokens in-between other tokens in the list are preserved. 46 | 47 | :param tokens: 48 | A list of :class:`~.token_data.Token` or 49 | :class:`~.token_data.ContainerToken`. 50 | :return: 51 | A new sub-sequence of the list. 52 | 53 | """ 54 | for i, token in enumerate(tokens): 55 | if token.type != 'S': 56 | break 57 | else: 58 | return [] # only whitespace 59 | tokens = tokens[i:] 60 | while tokens and tokens[-1].type == 'S': 61 | tokens.pop() 62 | return tokens 63 | 64 | 65 | def remove_whitespace(tokens): 66 | """Remove any top-level whitespace in a token list. 67 | 68 | Whitespace tokens inside recursive :class:`~.token_data.ContainerToken` 69 | are preserved. 70 | 71 | :param tokens: 72 | A list of :class:`~.token_data.Token` or 73 | :class:`~.token_data.ContainerToken`. 74 | :return: 75 | A new sub-sequence of the list. 76 | 77 | """ 78 | return [token for token in tokens if token.type != 'S'] 79 | 80 | 81 | def validate_value(tokens): 82 | """Validate a property value. 83 | 84 | :param tokens: 85 | an iterable of tokens 86 | :raises: 87 | :class:`ParseError` if there is any invalid token for the 'value' 88 | production of the core grammar. 89 | 90 | """ 91 | for token in tokens: 92 | type_ = token.type 93 | if type_ == '{': 94 | validate_block(token.content, 'property value') 95 | else: 96 | validate_any(token, 'property value') 97 | 98 | def validate_block(tokens, context): 99 | """ 100 | :raises: 101 | :class:`ParseError` if there is any invalid token for the 'block' 102 | production of the core grammar. 103 | :param tokens: an iterable of tokens 104 | :param context: a string for the 'unexpected in ...' message 105 | 106 | """ 107 | for token in tokens: 108 | type_ = token.type 109 | if type_ == '{': 110 | validate_block(token.content, context) 111 | elif type_ not in (';', 'ATKEYWORD'): 112 | validate_any(token, context) 113 | 114 | 115 | def validate_any(token, context): 116 | """ 117 | :raises: 118 | :class:`ParseError` if this is an invalid token for the 119 | 'any' production of the core grammar. 120 | :param token: a single token 121 | :param context: a string for the 'unexpected in ...' message 122 | 123 | """ 124 | type_ = token.type 125 | if type_ in ('FUNCTION', '(', '['): 126 | for token in token.content: 127 | validate_any(token, type_) 128 | elif type_ not in ('S', 'IDENT', 'DIMENSION', 'PERCENTAGE', 'NUMBER', 129 | 'INTEGER', 'URI', 'DELIM', 'STRING', 'HASH', ':', 130 | 'UNICODE-RANGE'): 131 | if type_ in ('}', ')', ']'): 132 | adjective = 'unmatched' 133 | else: 134 | adjective = 'unexpected' 135 | raise ParseError(token, 136 | '{0} {1} token in {2}'.format(adjective, type_, context)) 137 | 138 | 139 | class ParseError(ValueError): 140 | """Details about a CSS syntax error. Usually indicates that something 141 | (a rule or a declaration) was ignored and will not appear as a parsed 142 | object. 143 | 144 | This exception is typically logged in a list rather than being propagated 145 | to the user API. 146 | 147 | .. attribute:: line 148 | 149 | Source line where the error occured. 150 | 151 | .. attribute:: column 152 | 153 | Column in the source line where the error occured. 154 | 155 | .. attribute:: reason 156 | 157 | What happend (a string). 158 | 159 | """ 160 | def __init__(self, subject, reason): 161 | self.line = subject.line 162 | self.column = subject.column 163 | self.reason = reason 164 | super(ParseError, self).__init__( 165 | 'Parse error at {0.line}:{0.column}, {0.reason}'.format(self)) 166 | -------------------------------------------------------------------------------- /tinycsscheme/tinycss/tests/__init__.py: -------------------------------------------------------------------------------- 1 | # coding: utf8 2 | """ 3 | Test suite for tinycss 4 | ---------------------- 5 | 6 | :copyright: (c) 2012 by Simon Sapin. 7 | :license: BSD, see LICENSE for more details. 8 | """ 9 | 10 | 11 | from __future__ import unicode_literals 12 | 13 | 14 | def assert_errors(errors, expected_errors): 15 | """Test not complete error messages but only substrings.""" 16 | assert len(errors) == len(expected_errors) 17 | for error, expected in zip(errors, expected_errors): 18 | assert expected in str(error) 19 | -------------------------------------------------------------------------------- /tinycsscheme/tinycss/tests/speed.py: -------------------------------------------------------------------------------- 1 | # coding: utf8 2 | """ 3 | Speed tests 4 | ----------- 5 | 6 | Note: this file is not named test_*.py as it is not part of the 7 | test suite ran by pytest. 8 | 9 | :copyright: (c) 2012 by Simon Sapin. 10 | :license: BSD, see LICENSE for more details. 11 | """ 12 | 13 | 14 | from __future__ import unicode_literals, division 15 | 16 | import sys 17 | import os.path 18 | import contextlib 19 | import timeit 20 | import functools 21 | 22 | from cssutils import parseString 23 | 24 | from .. import tokenizer 25 | from ..css21 import CSS21Parser 26 | from ..parsing import remove_whitespace 27 | 28 | 29 | CSS_REPEAT = 4 30 | TIMEIT_REPEAT = 3 31 | TIMEIT_NUMBER = 20 32 | 33 | 34 | def load_css(): 35 | filename = os.path.join(os.path.dirname(__file__), 36 | '..', '..', 'docs', '_static', 'custom.css') 37 | with open(filename, 'rb') as fd: 38 | return b'\n'.join([fd.read()] * CSS_REPEAT) 39 | 40 | 41 | # Pre-load so that I/O is not measured 42 | CSS = load_css() 43 | 44 | 45 | @contextlib.contextmanager 46 | def install_tokenizer(name): 47 | original = tokenizer.tokenize_flat 48 | try: 49 | tokenizer.tokenize_flat = getattr(tokenizer, name) 50 | yield 51 | finally: 52 | tokenizer.tokenize_flat = original 53 | 54 | 55 | def parse(tokenizer_name): 56 | with install_tokenizer(tokenizer_name): 57 | stylesheet = CSS21Parser().parse_stylesheet_bytes(CSS) 58 | result = [] 59 | for rule in stylesheet.rules: 60 | selector = rule.selector.as_css() 61 | declarations = [ 62 | (declaration.name, len(list(remove_whitespace(declaration.value)))) 63 | for declaration in rule.declarations] 64 | result.append((selector, declarations)) 65 | return result 66 | 67 | parse_cython = functools.partial(parse, 'cython_tokenize_flat') 68 | parse_python = functools.partial(parse, 'python_tokenize_flat') 69 | 70 | 71 | def parse_cssutils(): 72 | stylesheet = parseString(CSS) 73 | result = [] 74 | for rule in stylesheet.cssRules: 75 | selector = rule.selectorText 76 | declarations = [ 77 | (declaration.name, len(list(declaration.propertyValue))) 78 | for declaration in rule.style.getProperties(all=True)] 79 | result.append((selector, declarations)) 80 | return result 81 | 82 | 83 | def check_consistency(): 84 | result = parse_python() 85 | #import pprint 86 | #pprint.pprint(result) 87 | assert len(result) > 0 88 | if tokenizer.cython_tokenize_flat: 89 | assert parse_cython() == result 90 | assert parse_cssutils() == result 91 | version = '.'.join(map(str, sys.version_info[:3])) 92 | print('Python {}, consistency OK.'.format(version)) 93 | 94 | 95 | def warm_up(): 96 | is_pypy = hasattr(sys, 'pypy_translation_info') 97 | if is_pypy: 98 | print('Warming up for PyPy...') 99 | for i in range(80): 100 | for i in range(10): 101 | parse_python() 102 | parse_cssutils() 103 | sys.stdout.write('.') 104 | sys.stdout.flush() 105 | sys.stdout.write('\n') 106 | 107 | 108 | def time(function): 109 | seconds = timeit.Timer(function).repeat(TIMEIT_REPEAT, TIMEIT_NUMBER) 110 | miliseconds = int(min(seconds) * 1000) 111 | return miliseconds 112 | 113 | 114 | def run(): 115 | if tokenizer.cython_tokenize_flat: 116 | data_set = [ 117 | ('tinycss + speedups ', parse_cython), 118 | ] 119 | else: 120 | print('Speedups are NOT available.') 121 | data_set = [] 122 | data_set += [ 123 | ('tinycss WITHOUT speedups', parse_python), 124 | ('cssutils ', parse_cssutils), 125 | ] 126 | label, function = data_set.pop(0) 127 | ref = time(function) 128 | print('{} {} ms'.format(label, ref)) 129 | for label, function in data_set: 130 | result = time(function) 131 | print('{} {} ms {:.2f}x'.format(label, result, result / ref)) 132 | 133 | 134 | if __name__ == '__main__': 135 | check_consistency() 136 | warm_up() 137 | run() 138 | -------------------------------------------------------------------------------- /tinycsscheme/tinycss/tests/test_api.py: -------------------------------------------------------------------------------- 1 | # coding: utf8 2 | """ 3 | Tests for the public API 4 | ------------------------ 5 | 6 | :copyright: (c) 2012 by Simon Sapin. 7 | :license: BSD, see LICENSE for more details. 8 | """ 9 | 10 | 11 | from __future__ import unicode_literals 12 | 13 | from pytest import raises 14 | 15 | from .. import make_parser 16 | from ..page3 import CSSPage3Parser 17 | 18 | 19 | def test_make_parser(): 20 | class MyParser(object): 21 | def __init__(self, some_config): 22 | self.some_config = some_config 23 | 24 | parsers = [ 25 | make_parser(), 26 | make_parser('page3'), 27 | make_parser(CSSPage3Parser), 28 | make_parser(MyParser, some_config=42), 29 | make_parser(CSSPage3Parser, MyParser, some_config=42), 30 | make_parser(MyParser, 'page3', some_config=42), 31 | ] 32 | 33 | for parser, exp in zip(parsers, [False, True, True, False, True, True]): 34 | assert isinstance(parser, CSSPage3Parser) == exp 35 | 36 | for parser, exp in zip(parsers, [False, False, False, True, True, True]): 37 | assert isinstance(parser, MyParser) == exp 38 | 39 | for parser in parsers[3:]: 40 | assert parser.some_config == 42 41 | 42 | # Extra or missing named parameters 43 | raises(TypeError, make_parser, some_config=4) 44 | raises(TypeError, make_parser, 'page3', some_config=4) 45 | raises(TypeError, make_parser, MyParser) 46 | raises(TypeError, make_parser, MyParser, some_config=4, other_config=7) 47 | -------------------------------------------------------------------------------- /tinycsscheme/tinycss/tests/test_color3.py: -------------------------------------------------------------------------------- 1 | # coding: utf8 2 | """ 3 | Tests for the CSS 3 color parser 4 | -------------------------------- 5 | 6 | :copyright: (c) 2012 by Simon Sapin. 7 | :license: BSD, see LICENSE for more details. 8 | """ 9 | 10 | 11 | from __future__ import unicode_literals 12 | 13 | import pytest 14 | 15 | from ..color3 import parse_color_string, hsl_to_rgb 16 | 17 | 18 | @pytest.mark.parametrize(('css_source', 'expected_result'), [ 19 | ('', None), 20 | (' /* hey */\n', None), 21 | ('4', None), 22 | ('top', None), 23 | ('/**/transparent', (0, 0, 0, 0)), 24 | ('transparent', (0, 0, 0, 0)), 25 | (' transparent\n', (0, 0, 0, 0)), 26 | ('TransParent', (0, 0, 0, 0)), 27 | ('currentColor', 'currentColor'), 28 | ('CURRENTcolor', 'currentColor'), 29 | ('current_Color', None), 30 | 31 | ('black', (0, 0, 0, 1)), 32 | ('white', (1, 1, 1, 1)), 33 | ('fuchsia', (1, 0, 1, 1)), 34 | ('cyan', (0, 1, 1, 1)), 35 | ('CyAn', (0, 1, 1, 1)), 36 | ('darkkhaki', (189 / 255., 183 / 255., 107 / 255., 1)), 37 | 38 | ('#', None), 39 | ('#f', None), 40 | ('#ff', None), 41 | ('#fff', (1, 1, 1, 1)), 42 | ('#ffg', None), 43 | ('#ffff', None), 44 | ('#fffff', None), 45 | ('#ffffff', (1, 1, 1, 1)), 46 | ('#fffffg', None), 47 | ('#fffffff', None), 48 | ('#ffffffff', None), 49 | ('#fffffffff', None), 50 | 51 | ('#cba987', (203 / 255., 169 / 255., 135 / 255., 1)), 52 | ('#CbA987', (203 / 255., 169 / 255., 135 / 255., 1)), 53 | ('#1122aA', (17 / 255., 34 / 255., 170 / 255., 1)), 54 | ('#12a', (17 / 255., 34 / 255., 170 / 255., 1)), 55 | 56 | ('rgb(203, 169, 135)', (203 / 255., 169 / 255., 135 / 255., 1)), 57 | ('RGB(255, 255, 255)', (1, 1, 1, 1)), 58 | ('rgB(0, 0, 0)', (0, 0, 0, 1)), 59 | ('rgB(0, 51, 255)', (0, .2, 1, 1)), 60 | ('rgb(0,51,255)', (0, .2, 1, 1)), 61 | ('rgb(0\t, 51 ,255)', (0, .2, 1, 1)), 62 | ('rgb(/* R */0, /* G */51, /* B */255)', (0, .2, 1, 1)), 63 | ('rgb(-51, 306, 0)', (-.2, 1.2, 0, 1)), # out of 0..1 is allowed 64 | 65 | ('rgb(42%, 3%, 50%)', (.42, .03, .5, 1)), 66 | ('RGB(100%, 100%, 100%)', (1, 1, 1, 1)), 67 | ('rgB(0%, 0%, 0%)', (0, 0, 0, 1)), 68 | ('rgB(10%, 20%, 30%)', (.1, .2, .3, 1)), 69 | ('rgb(10%,20%,30%)', (.1, .2, .3, 1)), 70 | ('rgb(10%\t, 20% ,30%)', (.1, .2, .3, 1)), 71 | ('rgb(/* R */10%, /* G */20%, /* B */30%)', (.1, .2, .3, 1)), 72 | ('rgb(-12%, 110%, 1400%)', (-.12, 1.1, 14, 1)), # out of 0..1 is allowed 73 | 74 | ('rgb(10%, 50%, 0)', None), 75 | ('rgb(255, 50%, 0%)', None), 76 | ('rgb(0, 0 0)', None), 77 | ('rgb(0, 0, 0deg)', None), 78 | ('rgb(0, 0, light)', None), 79 | ('rgb()', None), 80 | ('rgb(0)', None), 81 | ('rgb(0, 0)', None), 82 | ('rgb(0, 0, 0, 0)', None), 83 | ('rgb(0%)', None), 84 | ('rgb(0%, 0%)', None), 85 | ('rgb(0%, 0%, 0%, 0%)', None), 86 | ('rgb(0%, 0%, 0%, 0)', None), 87 | 88 | ('rgba(0, 0, 0, 0)', (0, 0, 0, 0)), 89 | ('rgba(203, 169, 135, 0.3)', (203 / 255., 169 / 255., 135 / 255., 0.3)), 90 | ('RGBA(255, 255, 255, 0)', (1, 1, 1, 0)), 91 | ('rgBA(0, 51, 255, 1)', (0, 0.2, 1, 1)), 92 | ('rgba(0, 51, 255, 1.1)', (0, 0.2, 1, 1)), 93 | ('rgba(0, 51, 255, 37)', (0, 0.2, 1, 1)), 94 | ('rgba(0, 51, 255, 0.42)', (0, 0.2, 1, 0.42)), 95 | ('rgba(0, 51, 255, 0)', (0, 0.2, 1, 0)), 96 | ('rgba(0, 51, 255, -0.1)', (0, 0.2, 1, 0)), 97 | ('rgba(0, 51, 255, -139)', (0, 0.2, 1, 0)), 98 | 99 | ('rgba(42%, 3%, 50%, 0.3)', (.42, .03, .5, 0.3)), 100 | ('RGBA(100%, 100%, 100%, 0)', (1, 1, 1, 0)), 101 | ('rgBA(0%, 20%, 100%, 1)', (0, 0.2, 1, 1)), 102 | ('rgba(0%, 20%, 100%, 1.1)', (0, 0.2, 1, 1)), 103 | ('rgba(0%, 20%, 100%, 37)', (0, 0.2, 1, 1)), 104 | ('rgba(0%, 20%, 100%, 0.42)', (0, 0.2, 1, 0.42)), 105 | ('rgba(0%, 20%, 100%, 0)', (0, 0.2, 1, 0)), 106 | ('rgba(0%, 20%, 100%, -0.1)', (0, 0.2, 1, 0)), 107 | ('rgba(0%, 20%, 100%, -139)', (0, 0.2, 1, 0)), 108 | 109 | ('rgba(255, 255, 255, 0%)', None), 110 | ('rgba(10%, 50%, 0, 1)', None), 111 | ('rgba(255, 50%, 0%, 1)', None), 112 | ('rgba(0, 0, 0 0)', None), 113 | ('rgba(0, 0, 0, 0deg)', None), 114 | ('rgba(0, 0, 0, light)', None), 115 | ('rgba()', None), 116 | ('rgba(0)', None), 117 | ('rgba(0, 0, 0)', None), 118 | ('rgba(0, 0, 0, 0, 0)', None), 119 | ('rgba(0%)', None), 120 | ('rgba(0%, 0%)', None), 121 | ('rgba(0%, 0%, 0%)', None), 122 | ('rgba(0%, 0%, 0%, 0%)', None), 123 | ('rgba(0%, 0%, 0%, 0%, 0%)', None), 124 | 125 | ('HSL(0, 0%, 0%)', (0, 0, 0, 1)), 126 | ('hsL(0, 100%, 50%)', (1, 0, 0, 1)), 127 | ('hsl(60, 100%, 37.5%)', (0.75, 0.75, 0, 1)), 128 | ('hsl(780, 100%, 37.5%)', (0.75, 0.75, 0, 1)), 129 | ('hsl(-300, 100%, 37.5%)', (0.75, 0.75, 0, 1)), 130 | ('hsl(300, 50%, 50%)', (0.75, 0.25, 0.75, 1)), 131 | 132 | ('hsl(10, 50%, 0)', None), 133 | ('hsl(50%, 50%, 0%)', None), 134 | ('hsl(0, 0% 0%)', None), 135 | ('hsl(30deg, 100%, 100%)', None), 136 | ('hsl(0, 0%, light)', None), 137 | ('hsl()', None), 138 | ('hsl(0)', None), 139 | ('hsl(0, 0%)', None), 140 | ('hsl(0, 0%, 0%, 0%)', None), 141 | 142 | ('HSLA(-300, 100%, 37.5%, 1)', (0.75, 0.75, 0, 1)), 143 | ('hsLA(-300, 100%, 37.5%, 12)', (0.75, 0.75, 0, 1)), 144 | ('hsla(-300, 100%, 37.5%, 0.2)', (0.75, 0.75, 0, .2)), 145 | ('hsla(-300, 100%, 37.5%, 0)', (0.75, 0.75, 0, 0)), 146 | ('hsla(-300, 100%, 37.5%, -3)', (0.75, 0.75, 0, 0)), 147 | 148 | ('hsla(10, 50%, 0, 1)', None), 149 | ('hsla(50%, 50%, 0%, 1)', None), 150 | ('hsla(0, 0% 0%, 1)', None), 151 | ('hsla(30deg, 100%, 100%, 1)', None), 152 | ('hsla(0, 0%, light, 1)', None), 153 | ('hsla()', None), 154 | ('hsla(0)', None), 155 | ('hsla(0, 0%)', None), 156 | ('hsla(0, 0%, 0%, 50%)', None), 157 | ('hsla(0, 0%, 0%, 1, 0%)', None), 158 | 159 | ('cmyk(0, 0, 0, 0)', None), 160 | ]) 161 | def test_color(css_source, expected_result): 162 | result = parse_color_string(css_source) 163 | if isinstance(result, tuple): 164 | for got, expected in zip(result, expected_result): 165 | # Compensate for floating point errors: 166 | assert abs(got - expected) < 1e-10 167 | for i, attr in enumerate(['red', 'green', 'blue', 'alpha']): 168 | assert getattr(result, attr) == result[i] 169 | else: 170 | assert result == expected_result 171 | 172 | 173 | @pytest.mark.parametrize(('hsl', 'expected_rgb'), [ 174 | # http://en.wikipedia.org/wiki/HSL_and_HSV#Examples 175 | ((0, 0, 100 ), (1, 1, 1 )), 176 | ((127, 0, 100 ), (1, 1, 1 )), 177 | ((0, 0, 50 ), (0.5, 0.5, 0.5 )), 178 | ((127, 0, 50 ), (0.5, 0.5, 0.5 )), 179 | ((0, 0, 0 ), (0, 0, 0 )), 180 | ((127, 0, 0 ), (0, 0, 0 )), 181 | ((0, 100, 50 ), (1, 0, 0 )), 182 | ((60, 100, 37.5), (0.75, 0.75, 0 )), 183 | ((780, 100, 37.5), (0.75, 0.75, 0 )), 184 | ((-300, 100, 37.5), (0.75, 0.75, 0 )), 185 | ((120, 100, 25 ), (0, 0.5, 0 )), 186 | ((180, 100, 75 ), (0.5, 1, 1 )), 187 | ((240, 100, 75 ), (0.5, 0.5, 1 )), 188 | ((300, 50, 50 ), (0.75, 0.25, 0.75 )), 189 | ((61.8, 63.8, 39.3), (0.628, 0.643, 0.142)), 190 | ((251.1, 83.2, 51.1), (0.255, 0.104, 0.918)), 191 | ((134.9, 70.7, 39.6), (0.116, 0.675, 0.255)), 192 | ((49.5, 89.3, 49.7), (0.941, 0.785, 0.053)), 193 | ((283.7, 77.5, 54.2), (0.704, 0.187, 0.897)), 194 | ((14.3, 81.7, 62.4), (0.931, 0.463, 0.316)), 195 | ((56.9, 99.1, 76.5), (0.998, 0.974, 0.532)), 196 | ((162.4, 77.9, 44.7), (0.099, 0.795, 0.591)), 197 | ((248.3, 60.1, 37.3), (0.211, 0.149, 0.597)), 198 | ((240.5, 29, 60.7), (0.495, 0.493, 0.721)), 199 | ]) 200 | def test_hsl(hsl, expected_rgb): 201 | for got, expected in zip(hsl_to_rgb(*hsl), expected_rgb): 202 | # Compensate for floating point errors and Wikipedia’s rounding: 203 | assert abs(got - expected) < 0.001 204 | -------------------------------------------------------------------------------- /tinycsscheme/tinycss/tests/test_decoding.py: -------------------------------------------------------------------------------- 1 | # coding: utf8 2 | """ 3 | Tests for decoding bytes to Unicode 4 | ----------------------------------- 5 | 6 | :copyright: (c) 2012 by Simon Sapin. 7 | :license: BSD, see LICENSE for more details. 8 | """ 9 | 10 | 11 | from __future__ import unicode_literals 12 | 13 | import pytest 14 | 15 | from ..decoding import decode 16 | 17 | 18 | def params(css, encoding, use_bom=False, expect_error=False, **kwargs): 19 | """Nicer syntax to make a tuple.""" 20 | return css, encoding, use_bom, expect_error, kwargs 21 | 22 | 23 | @pytest.mark.parametrize(('css', 'encoding', 'use_bom', 'expect_error', 24 | 'kwargs'), [ 25 | params('', 'utf8'), # default to utf8 26 | params('𐂃', 'utf8'), 27 | params('é', 'latin1'), # utf8 fails, fall back on ShiftJIS 28 | params('£', 'ShiftJIS', expect_error=True), 29 | params('£', 'ShiftJIS', protocol_encoding='Shift-JIS'), 30 | params('£', 'ShiftJIS', linking_encoding='Shift-JIS'), 31 | params('£', 'ShiftJIS', document_encoding='Shift-JIS'), 32 | params('£', 'ShiftJIS', protocol_encoding='utf8', 33 | document_encoding='ShiftJIS'), 34 | params('@charset "utf8"; £', 'ShiftJIS', expect_error=True), 35 | params('@charset "utf£8"; £', 'ShiftJIS', expect_error=True), 36 | params('@charset "unknown-encoding"; £', 'ShiftJIS', expect_error=True), 37 | params('@charset "utf8"; £', 'ShiftJIS', document_encoding='ShiftJIS'), 38 | params('£', 'ShiftJIS', linking_encoding='utf8', 39 | document_encoding='ShiftJIS'), 40 | params('@charset "utf-32"; 𐂃', 'utf-32-be'), 41 | params('@charset "Shift-JIS"; £', 'ShiftJIS'), 42 | params('@charset "ISO-8859-8"; £', 'ShiftJIS', expect_error=True), 43 | params('𐂃', 'utf-16-le', expect_error=True), # no BOM 44 | params('𐂃', 'utf-16-le', use_bom=True), 45 | params('𐂃', 'utf-32-be', expect_error=True), 46 | params('𐂃', 'utf-32-be', use_bom=True), 47 | params('𐂃', 'utf-32-be', document_encoding='utf-32-be'), 48 | params('𐂃', 'utf-32-be', linking_encoding='utf-32-be'), 49 | params('@charset "utf-32-le"; 𐂃', 'utf-32-be', 50 | use_bom=True, expect_error=True), 51 | # protocol_encoding takes precedence over @charset 52 | params('@charset "ISO-8859-8"; £', 'ShiftJIS', 53 | protocol_encoding='Shift-JIS'), 54 | params('@charset "unknown-encoding"; £', 'ShiftJIS', 55 | protocol_encoding='Shift-JIS'), 56 | params('@charset "Shift-JIS"; £', 'ShiftJIS', 57 | protocol_encoding='utf8'), 58 | # @charset takes precedence over document_encoding 59 | params('@charset "Shift-JIS"; £', 'ShiftJIS', 60 | document_encoding='ISO-8859-8'), 61 | # @charset takes precedence over linking_encoding 62 | params('@charset "Shift-JIS"; £', 'ShiftJIS', 63 | linking_encoding='ISO-8859-8'), 64 | # linking_encoding takes precedence over document_encoding 65 | params('£', 'ShiftJIS', 66 | linking_encoding='Shift-JIS', document_encoding='ISO-8859-8'), 67 | ]) 68 | def test_decode(css, encoding, use_bom, expect_error, kwargs): 69 | # Workaround PyPy and CPython 3.0 bug: https://bugs.pypy.org/issue1094 70 | css = css.encode('utf16').decode('utf16') 71 | if use_bom: 72 | source = '\ufeff' + css 73 | else: 74 | source = css 75 | css_bytes = source.encode(encoding) 76 | result, result_encoding = decode(css_bytes, **kwargs) 77 | if expect_error: 78 | assert result != css, 'Unexpected unicode success' 79 | else: 80 | assert result == css, 'Unexpected unicode error' 81 | -------------------------------------------------------------------------------- /tinycsscheme/tinycss/tests/test_page3.py: -------------------------------------------------------------------------------- 1 | # coding: utf8 2 | """ 3 | Tests for the Paged Media 3 parser 4 | ---------------------------------- 5 | 6 | :copyright: (c) 2012 by Simon Sapin. 7 | :license: BSD, see LICENSE for more details. 8 | """ 9 | 10 | 11 | from __future__ import unicode_literals 12 | 13 | import pytest 14 | 15 | from ..page3 import CSSPage3Parser 16 | from .test_tokenizer import jsonify 17 | from . import assert_errors 18 | 19 | 20 | @pytest.mark.parametrize(('css', 'expected_selector', 21 | 'expected_specificity', 'expected_errors'), [ 22 | ('@page {}', (None, None), (0, 0, 0), []), 23 | 24 | ('@page :first {}', (None, 'first'), (0, 1, 0), []), 25 | ('@page:left{}', (None, 'left'), (0, 0, 1), []), 26 | ('@page :right {}', (None, 'right'), (0, 0, 1), []), 27 | ('@page :blank{}', (None, 'blank'), (0, 1, 0), []), 28 | ('@page :last {}', None, None, ['invalid @page selector']), 29 | ('@page : first {}', None, None, ['invalid @page selector']), 30 | 31 | ('@page foo:first {}', ('foo', 'first'), (1, 1, 0), []), 32 | ('@page bar :left {}', ('bar', 'left'), (1, 0, 1), []), 33 | (r'@page \26:right {}', ('&', 'right'), (1, 0, 1), []), 34 | 35 | ('@page foo {}', ('foo', None), (1, 0, 0), []), 36 | (r'@page \26 {}', ('&', None), (1, 0, 0), []), 37 | 38 | ('@page foo fist {}', None, None, ['invalid @page selector']), 39 | ('@page foo, bar {}', None, None, ['invalid @page selector']), 40 | ('@page foo&first {}', None, None, ['invalid @page selector']), 41 | ]) 42 | def test_selectors(css, expected_selector, expected_specificity, 43 | expected_errors): 44 | stylesheet = CSSPage3Parser().parse_stylesheet(css) 45 | assert_errors(stylesheet.errors, expected_errors) 46 | 47 | if stylesheet.rules: 48 | assert len(stylesheet.rules) == 1 49 | rule = stylesheet.rules[0] 50 | assert rule.at_keyword == '@page' 51 | selector = rule.selector 52 | assert rule.specificity == expected_specificity 53 | else: 54 | selector = None 55 | assert selector == expected_selector 56 | 57 | 58 | @pytest.mark.parametrize(('css', 'expected_declarations', 59 | 'expected_rules','expected_errors'), [ 60 | ('@page {}', [], [], []), 61 | ('@page { foo: 4; bar: z }', 62 | [('foo', [('INTEGER', 4)]), ('bar', [('IDENT', 'z')])], [], []), 63 | ('''@page { foo: 4; 64 | @top-center { content: "Awesome Title" } 65 | @bottom-left { content: counter(page) } 66 | bar: z 67 | }''', 68 | [('foo', [('INTEGER', 4)]), ('bar', [('IDENT', 'z')])], 69 | [('@top-center', [('content', [('STRING', 'Awesome Title')])]), 70 | ('@bottom-left', [('content', [ 71 | ('FUNCTION', 'counter', [('IDENT', 'page')])])])], 72 | []), 73 | ('''@page { foo: 4; 74 | @bottom-top { content: counter(page) } 75 | bar: z 76 | }''', 77 | [('foo', [('INTEGER', 4)]), ('bar', [('IDENT', 'z')])], 78 | [], 79 | ['unknown at-rule in @page context: @bottom-top']), 80 | 81 | ('@page{} @top-right{}', [], [], [ 82 | '@top-right rule not allowed in stylesheet']), 83 | ('@page{ @top-right 4 {} }', [], [], [ 84 | 'unexpected INTEGER token in @top-right rule header']), 85 | # Not much error recovery tests here. This should be covered in test_css21 86 | ]) 87 | def test_content(css, expected_declarations, expected_rules, expected_errors): 88 | stylesheet = CSSPage3Parser().parse_stylesheet(css) 89 | assert_errors(stylesheet.errors, expected_errors) 90 | 91 | def declarations(rule): 92 | return [(decl.name, list(jsonify(decl.value))) 93 | for decl in rule.declarations] 94 | 95 | assert len(stylesheet.rules) == 1 96 | rule = stylesheet.rules[0] 97 | assert rule.at_keyword == '@page' 98 | assert declarations(rule) == expected_declarations 99 | rules = [(margin_rule.at_keyword, declarations(margin_rule)) 100 | for margin_rule in rule.at_rules] 101 | assert rules == expected_rules 102 | -------------------------------------------------------------------------------- /tinycsscheme/tinycss/tokenizer.py: -------------------------------------------------------------------------------- 1 | # coding: utf8 2 | """ 3 | tinycss.tokenizer 4 | ----------------- 5 | 6 | Tokenizer for the CSS core syntax: 7 | http://www.w3.org/TR/CSS21/syndata.html#tokenization 8 | 9 | This is the pure-python implementation. See also speedups.pyx 10 | 11 | :copyright: (c) 2012 by Simon Sapin. 12 | :license: BSD, see LICENSE for more details. 13 | """ 14 | 15 | from __future__ import unicode_literals 16 | 17 | from . import token_data 18 | 19 | 20 | def tokenize_flat(css_source, ignore_comments=True, 21 | # Make these local variable to avoid global lookups in the loop 22 | tokens_dispatch=token_data.TOKEN_DISPATCH, 23 | unicode_unescape=token_data.UNICODE_UNESCAPE, 24 | newline_unescape=token_data.NEWLINE_UNESCAPE, 25 | simple_unescape=token_data.SIMPLE_UNESCAPE, 26 | find_newlines=token_data.FIND_NEWLINES, 27 | Token=token_data.Token, 28 | len=len, 29 | int=int, 30 | float=float, 31 | list=list, 32 | _None=None, 33 | ): 34 | """ 35 | :param css_source: 36 | CSS as an unicode string 37 | :param ignore_comments: 38 | if true (the default) comments will not be included in the 39 | return value 40 | :return: 41 | An iterator of :class:`Token` 42 | 43 | """ 44 | 45 | pos = 0 46 | line = 1 47 | column = 1 48 | source_len = len(css_source) 49 | tokens = [] 50 | while pos < source_len: 51 | char = css_source[pos] 52 | if char in ':;{}()[]': 53 | type_ = char 54 | css_value = char 55 | else: 56 | codepoint = min(ord(char), 160) 57 | for _index, type_, regexp in tokens_dispatch[codepoint]: 58 | match = regexp(css_source, pos) 59 | if match: 60 | # First match is the longest. See comments on TOKENS above. 61 | css_value = match.group() 62 | break 63 | else: 64 | # No match. 65 | # "Any other character not matched by the above rules, 66 | # and neither a single nor a double quote." 67 | # ... but quotes at the start of a token are always matched 68 | # by STRING or BAD_STRING. So DELIM is any single character. 69 | type_ = 'DELIM' 70 | css_value = char 71 | length = len(css_value) 72 | next_pos = pos + length 73 | 74 | # A BAD_COMMENT is a comment at EOF. Ignore it too. 75 | if not (ignore_comments and type_ in ('COMMENT', 'BAD_COMMENT')): 76 | # Parse numbers, extract strings and URIs, unescape 77 | unit = _None 78 | if type_ == 'DIMENSION': 79 | value = match.group(1) 80 | value = float(value) if '.' in value else int(value) 81 | unit = match.group(2) 82 | unit = simple_unescape(unit) 83 | unit = unicode_unescape(unit) 84 | unit = unit.lower() # normalize 85 | elif type_ == 'PERCENTAGE': 86 | value = css_value[:-1] 87 | value = float(value) if '.' in value else int(value) 88 | unit = '%' 89 | elif type_ == 'NUMBER': 90 | value = css_value 91 | if '.' in value: 92 | value = float(value) 93 | else: 94 | value = int(value) 95 | type_ = 'INTEGER' 96 | elif type_ in ('IDENT', 'ATKEYWORD', 'HASH', 'FUNCTION'): 97 | value = simple_unescape(css_value) 98 | value = unicode_unescape(value) 99 | elif type_ == 'URI': 100 | value = match.group(1) 101 | if value and value[0] in '"\'': 102 | value = value[1:-1] # Remove quotes 103 | value = newline_unescape(value) 104 | value = simple_unescape(value) 105 | value = unicode_unescape(value) 106 | elif type_ == 'STRING': 107 | value = css_value[1:-1] # Remove quotes 108 | value = newline_unescape(value) 109 | value = simple_unescape(value) 110 | value = unicode_unescape(value) 111 | # BAD_STRING can only be one of: 112 | # * Unclosed string at the end of the stylesheet: 113 | # Close the string, but this is not an error. 114 | # Make it a "good" STRING token. 115 | # * Unclosed string at the (unescaped) end of the line: 116 | # Close the string, but this is an error. 117 | # Leave it as a BAD_STRING, don’t bother parsing it. 118 | # See http://www.w3.org/TR/CSS21/syndata.html#parsing-errors 119 | elif type_ == 'BAD_STRING' and next_pos == source_len: 120 | type_ = 'STRING' 121 | value = css_value[1:] # Remove quote 122 | value = newline_unescape(value) 123 | value = simple_unescape(value) 124 | value = unicode_unescape(value) 125 | else: 126 | value = css_value 127 | tokens.append(Token(type_, css_value, value, unit, line, column)) 128 | 129 | pos = next_pos 130 | newlines = list(find_newlines(css_value)) 131 | if newlines: 132 | line += len(newlines) 133 | # Add 1 to have lines start at column 1, not 0 134 | column = length - newlines[-1].end() + 1 135 | else: 136 | column += length 137 | return tokens 138 | 139 | 140 | def regroup(tokens): 141 | """ 142 | Match pairs of tokens: () [] {} function() 143 | (Strings in "" or '' are taken care of by the tokenizer.) 144 | 145 | Opening tokens are replaced by a :class:`ContainerToken`. 146 | Closing tokens are removed. Unmatched closing tokens are invalid 147 | but left as-is. All nested structures that are still open at 148 | the end of the stylesheet are implicitly closed. 149 | 150 | :param tokens: 151 | a *flat* iterable of tokens, as returned by :func:`tokenize_flat`. 152 | :return: 153 | A tree of tokens. 154 | 155 | """ 156 | # "global" objects for the inner recursion 157 | pairs = {'FUNCTION': ')', '(': ')', '[': ']', '{': '}'} 158 | tokens = iter(tokens) 159 | eof = [False] 160 | 161 | def _regroup_inner(stop_at=None, 162 | tokens=tokens, pairs=pairs, eof=eof, 163 | ContainerToken=token_data.ContainerToken, 164 | FunctionToken=token_data.FunctionToken): 165 | for token in tokens: 166 | type_ = token.type 167 | if type_ == stop_at: 168 | return 169 | 170 | end = pairs.get(type_) 171 | if end is None: 172 | yield token # Not a grouping token 173 | else: 174 | assert not isinstance(token, ContainerToken), ( 175 | 'Token looks already grouped: {0}'.format(token)) 176 | content = list(_regroup_inner(end)) 177 | if eof[0]: 178 | end = '' # Implicit end of structure at EOF. 179 | if type_ == 'FUNCTION': 180 | yield FunctionToken(token.type, token.as_css(), end, 181 | token.value, content, 182 | token.line, token.column) 183 | else: 184 | yield ContainerToken(token.type, token.as_css(), end, 185 | content, 186 | token.line, token.column) 187 | else: 188 | eof[0] = True # end of file/stylesheet 189 | return _regroup_inner() 190 | 191 | 192 | def tokenize_grouped(css_source, ignore_comments=True): 193 | """ 194 | :param css_source: 195 | CSS as an unicode string 196 | :param ignore_comments: 197 | if true (the default) comments will not be included in the 198 | return value 199 | :return: 200 | An iterator of :class:`Token` 201 | 202 | """ 203 | return regroup(tokenize_flat(css_source, ignore_comments)) 204 | 205 | 206 | # Optional Cython version of tokenize_flat 207 | # Make both versions available with explicit names for tests. 208 | python_tokenize_flat = tokenize_flat 209 | try: 210 | from . import speedups 211 | except ImportError: 212 | cython_tokenize_flat = None 213 | else: # pragma: no cover 214 | cython_tokenize_flat = speedups.tokenize_flat 215 | # Default to the Cython version if available 216 | tokenize_flat = cython_tokenize_flat 217 | -------------------------------------------------------------------------------- /tinycsscheme/tinycss/version.py: -------------------------------------------------------------------------------- 1 | VERSION = '0.3' 2 | --------------------------------------------------------------------------------