├── .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 | 
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 |
--------------------------------------------------------------------------------