├── .gitignore ├── AeSVG.json ├── LICENSE ├── README.md ├── TestImage.png ├── otls ├── ae__SVG_Export.hda └── ae__SVG_Import.hda └── python3.7libs ├── svg ├── __init__.py └── path │ ├── __init__.py │ ├── parser.py │ └── path.py └── tinycss ├── __init__.py ├── color3.py ├── css21.py ├── decoding.py ├── fonts3.py ├── page3.py ├── parsing.py ├── token_data.py ├── tokenizer.py └── version.py /.gitignore: -------------------------------------------------------------------------------- 1 | # backup dirs 2 | otls/backup 3 | 4 | # backup files 5 | *_bak*.hip 6 | 7 | # compiled python files 8 | *.pyc 9 | 10 | .vs 11 | .DS_Store 12 | Thumbs.db 13 | -------------------------------------------------------------------------------- /AeSVG.json: -------------------------------------------------------------------------------- 1 | { 2 | "env": [ 3 | { 4 | "AESVG": "PATH/TO/AE_SVG" 5 | } 6 | ], 7 | "path": [ 8 | "$AESVG" 9 | ] 10 | } -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | GNU LESSER GENERAL PUBLIC LICENSE 2 | Version 3, 29 June 2007 3 | 4 | Copyright (C) 2007 Free Software Foundation, Inc. 5 | Everyone is permitted to copy and distribute verbatim copies 6 | of this license document, but changing it is not allowed. 7 | 8 | 9 | This version of the GNU Lesser General Public License incorporates 10 | the terms and conditions of version 3 of the GNU General Public 11 | License, supplemented by the additional permissions listed below. 12 | 13 | 0. Additional Definitions. 14 | 15 | As used herein, "this License" refers to version 3 of the GNU Lesser 16 | General Public License, and the "GNU GPL" refers to version 3 of the GNU 17 | General Public License. 18 | 19 | "The Library" refers to a covered work governed by this License, 20 | other than an Application or a Combined Work as defined below. 21 | 22 | An "Application" is any work that makes use of an interface provided 23 | by the Library, but which is not otherwise based on the Library. 24 | Defining a subclass of a class defined by the Library is deemed a mode 25 | of using an interface provided by the Library. 26 | 27 | A "Combined Work" is a work produced by combining or linking an 28 | Application with the Library. The particular version of the Library 29 | with which the Combined Work was made is also called the "Linked 30 | Version". 31 | 32 | The "Minimal Corresponding Source" for a Combined Work means the 33 | Corresponding Source for the Combined Work, excluding any source code 34 | for portions of the Combined Work that, considered in isolation, are 35 | based on the Application, and not on the Linked Version. 36 | 37 | The "Corresponding Application Code" for a Combined Work means the 38 | object code and/or source code for the Application, including any data 39 | and utility programs needed for reproducing the Combined Work from the 40 | Application, but excluding the System Libraries of the Combined Work. 41 | 42 | 1. Exception to Section 3 of the GNU GPL. 43 | 44 | You may convey a covered work under sections 3 and 4 of this License 45 | without being bound by section 3 of the GNU GPL. 46 | 47 | 2. Conveying Modified Versions. 48 | 49 | If you modify a copy of the Library, and, in your modifications, a 50 | facility refers to a function or data to be supplied by an Application 51 | that uses the facility (other than as an argument passed when the 52 | facility is invoked), then you may convey a copy of the modified 53 | version: 54 | 55 | a) under this License, provided that you make a good faith effort to 56 | ensure that, in the event an Application does not supply the 57 | function or data, the facility still operates, and performs 58 | whatever part of its purpose remains meaningful, or 59 | 60 | b) under the GNU GPL, with none of the additional permissions of 61 | this License applicable to that copy. 62 | 63 | 3. Object Code Incorporating Material from Library Header Files. 64 | 65 | The object code form of an Application may incorporate material from 66 | a header file that is part of the Library. You may convey such object 67 | code under terms of your choice, provided that, if the incorporated 68 | material is not limited to numerical parameters, data structure 69 | layouts and accessors, or small macros, inline functions and templates 70 | (ten or fewer lines in length), you do both of the following: 71 | 72 | a) Give prominent notice with each copy of the object code that the 73 | Library is used in it and that the Library and its use are 74 | covered by this License. 75 | 76 | b) Accompany the object code with a copy of the GNU GPL and this license 77 | document. 78 | 79 | 4. Combined Works. 80 | 81 | You may convey a Combined Work under terms of your choice that, 82 | taken together, effectively do not restrict modification of the 83 | portions of the Library contained in the Combined Work and reverse 84 | engineering for debugging such modifications, if you also do each of 85 | the following: 86 | 87 | a) Give prominent notice with each copy of the Combined Work that 88 | the Library is used in it and that the Library and its use are 89 | covered by this License. 90 | 91 | b) Accompany the Combined Work with a copy of the GNU GPL and this license 92 | document. 93 | 94 | c) For a Combined Work that displays copyright notices during 95 | execution, include the copyright notice for the Library among 96 | these notices, as well as a reference directing the user to the 97 | copies of the GNU GPL and this license document. 98 | 99 | d) Do one of the following: 100 | 101 | 0) Convey the Minimal Corresponding Source under the terms of this 102 | License, and the Corresponding Application Code in a form 103 | suitable for, and under terms that permit, the user to 104 | recombine or relink the Application with a modified version of 105 | the Linked Version to produce a modified Combined Work, in the 106 | manner specified by section 6 of the GNU GPL for conveying 107 | Corresponding Source. 108 | 109 | 1) Use a suitable shared library mechanism for linking with the 110 | Library. A suitable mechanism is one that (a) uses at run time 111 | a copy of the Library already present on the user's computer 112 | system, and (b) will operate properly with a modified version 113 | of the Library that is interface-compatible with the Linked 114 | Version. 115 | 116 | e) Provide Installation Information, but only if you would otherwise 117 | be required to provide such information under section 6 of the 118 | GNU GPL, and only to the extent that such information is 119 | necessary to install and execute a modified version of the 120 | Combined Work produced by recombining or relinking the 121 | Application with a modified version of the Linked Version. (If 122 | you use option 4d0, the Installation Information must accompany 123 | the Minimal Corresponding Source and Corresponding Application 124 | Code. If you use option 4d1, you must provide the Installation 125 | Information in the manner specified by section 6 of the GNU GPL 126 | for conveying Corresponding Source.) 127 | 128 | 5. Combined Libraries. 129 | 130 | You may place library facilities that are a work based on the 131 | Library side by side in a single library together with other library 132 | facilities that are not Applications and are not covered by this 133 | License, and convey such a combined library under terms of your 134 | choice, if you do both of the following: 135 | 136 | a) Accompany the combined library with a copy of the same work based 137 | on the Library, uncombined with any other library facilities, 138 | conveyed under the terms of this License. 139 | 140 | b) Give prominent notice with the combined library that part of it 141 | is a work based on the Library, and explaining where to find the 142 | accompanying uncombined form of the same work. 143 | 144 | 6. Revised Versions of the GNU Lesser General Public License. 145 | 146 | The Free Software Foundation may publish revised and/or new versions 147 | of the GNU Lesser General Public License from time to time. Such new 148 | versions will be similar in spirit to the present version, but may 149 | differ in detail to address new problems or concerns. 150 | 151 | Each version is given a distinguishing version number. If the 152 | Library as you received it specifies that a certain numbered version 153 | of the GNU Lesser General Public License "or any later version" 154 | applies to it, you have the option of following the terms and 155 | conditions either of that published version or of any later version 156 | published by the Free Software Foundation. If the Library as you 157 | received it does not specify a version number of the GNU Lesser 158 | General Public License, you may choose any version of the GNU Lesser 159 | General Public License ever published by the Free Software Foundation. 160 | 161 | If the Library as you received it specifies that a proxy can decide 162 | whether future versions of the GNU Lesser General Public License shall 163 | apply, that proxy's public statement of acceptance of any version is 164 | permanent authorization for you to choose that version for the 165 | Library. 166 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ae_SVG 2 | Python-based HDAs for importing and exporting SVG files in Houdini 3 | 4 | ![Demo](TestImage.png) 5 | 6 | # Installation (Houdini 17.5+) 7 | 8 | These SVG tools are included in Aelib (https://github.com/Aeoll/Aelib) - my large Houdini toolkit and HDA collection. I would highly recommend installing Aelib rather than just these SVG tools in isolation. 9 | 10 | If you would like to install just these 2 HDAs: 11 | * Download and extract the repository and move it to any location 12 | * Create a folder called 'packages' in your Houdini home directory (e.g C:/Users/MY_USER/Documents/houdini19.0) if it does not exist already 13 | * Copy the AeSVG.json file into the packages folder 14 | * Edit the json file to point to the ae_SVG parent directory (edit the "AESVG" line) 15 | * For more information on how package files work, see [HERE](https://www.sidefx.com/docs/houdini/ref/plugins.html) 16 | 17 | Do not perform this install alongside a full Aelib install as you will create duplicate HDAs! 18 | 19 | # Usage 20 | 21 | ## Importer 22 | SOP level HDA which loads SVG files. Tested with a variety of SVG 1.1 files exported from Illustrator and Inkscape. 23 | Features: 24 | - Supports Line, Polyline, Polygon, Circle, Rect and Path elements (Cubic bezier paths - open or closed) 25 | - Loads Fill, Stroke, Stroke-Weight and Opacity data and stores these as Primitive attributes 26 | - Options for outlining strokes, hole-ing compound shapes and converting bezier curves to polygons 27 | - Supports Compound shapes 28 | - Supports path grouping (creates primitive groups) 29 | 30 | Current Limitations: 31 | - Does not support 'Ellipse' elements (convert these to bezier paths before saving the SVG) 32 | - Does not support Clipping Paths (Error thrown) 33 | 34 | # Exporter 35 | SOP level HDA which saves the contents of a Houdini scene to SVG format 36 | - Usable but not guaranteed stable. 37 | - Supports Polygons, Polylines and Bezier Curves 38 | - Colour and stroke weight (pscale) data are saved in the SVG -------------------------------------------------------------------------------- /TestImage.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Aeoll/ae_SVG/d08804835b556a2a215554bd957e506cc45961fc/TestImage.png -------------------------------------------------------------------------------- /otls/ae__SVG_Export.hda: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Aeoll/ae_SVG/d08804835b556a2a215554bd957e506cc45961fc/otls/ae__SVG_Export.hda -------------------------------------------------------------------------------- /otls/ae__SVG_Import.hda: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Aeoll/ae_SVG/d08804835b556a2a215554bd957e506cc45961fc/otls/ae__SVG_Import.hda -------------------------------------------------------------------------------- /python3.7libs/svg/__init__.py: -------------------------------------------------------------------------------- 1 | __path__ = __import__("pkgutil").extend_path(__path__, __name__) 2 | -------------------------------------------------------------------------------- /python3.7libs/svg/path/__init__.py: -------------------------------------------------------------------------------- 1 | from .path import Path, Move, Line, Arc, Close # noqa: 401 2 | from .path import CubicBezier, QuadraticBezier # noqa: 401 3 | from .parser import parse_path # noqa: 401 4 | -------------------------------------------------------------------------------- /python3.7libs/svg/path/parser.py: -------------------------------------------------------------------------------- 1 | # SVG Path specification parser 2 | 3 | import re 4 | from . import path 5 | 6 | COMMANDS = set("MmZzLlHhVvCcSsQqTtAa") 7 | UPPERCASE = set("MZLHVCSQTA") 8 | 9 | COMMAND_RE = re.compile(r"([MmZzLlHhVvCcSsQqTtAa])") 10 | FLOAT_RE = re.compile(r"[-+]?[0-9]*\.?[0-9]+(?:[eE][-+]?[0-9]+)?") 11 | 12 | 13 | def _tokenize_path(pathdef): 14 | for x in COMMAND_RE.split(pathdef): 15 | if x in COMMANDS: 16 | yield x 17 | for token in FLOAT_RE.findall(x): 18 | yield token 19 | 20 | 21 | def parse_path(pathdef, current_pos=0j): 22 | # In the SVG specs, initial movetos are absolute, even if 23 | # specified as 'm'. This is the default behavior here as well. 24 | # But if you pass in a current_pos variable, the initial moveto 25 | # will be relative to that current_pos. This is useful. 26 | elements = list(_tokenize_path(pathdef)) 27 | # Reverse for easy use of .pop() 28 | elements.reverse() 29 | 30 | segments = path.Path() 31 | start_pos = None 32 | command = None 33 | 34 | while elements: 35 | 36 | if elements[-1] in COMMANDS: 37 | # New command. 38 | last_command = command # Used by S and T 39 | command = elements.pop() 40 | absolute = command in UPPERCASE 41 | command = command.upper() 42 | else: 43 | # If this element starts with numbers, it is an implicit command 44 | # and we don't change the command. Check that it's allowed: 45 | if command is None: 46 | raise ValueError( 47 | "Unallowed implicit command in %s, position %s" 48 | % (pathdef, len(pathdef.split()) - len(elements)) 49 | ) 50 | last_command = command # Used by S and T 51 | 52 | if command == "M": 53 | # Moveto command. 54 | x = elements.pop() 55 | y = elements.pop() 56 | pos = float(x) + float(y) * 1j 57 | if absolute: 58 | current_pos = pos 59 | else: 60 | current_pos += pos 61 | segments.append(path.Move(current_pos)) 62 | # when M is called, reset start_pos 63 | # This behavior of Z is defined in svg spec: 64 | # http://www.w3.org/TR/SVG/paths.html#PathDataClosePathCommand 65 | start_pos = current_pos 66 | 67 | # Implicit moveto commands are treated as lineto commands. 68 | # So we set command to lineto here, in case there are 69 | # further implicit commands after this moveto. 70 | command = "L" 71 | 72 | elif command == "Z": 73 | # Close path 74 | segments.append(path.Close(current_pos, start_pos)) 75 | current_pos = start_pos 76 | start_pos = None 77 | command = None # You can't have implicit commands after closing. 78 | 79 | elif command == "L": 80 | x = elements.pop() 81 | y = elements.pop() 82 | pos = float(x) + float(y) * 1j 83 | if not absolute: 84 | pos += current_pos 85 | segments.append(path.Line(current_pos, pos)) 86 | current_pos = pos 87 | 88 | elif command == "H": 89 | x = elements.pop() 90 | pos = float(x) + current_pos.imag * 1j 91 | if not absolute: 92 | pos += current_pos.real 93 | segments.append(path.Line(current_pos, pos)) 94 | current_pos = pos 95 | 96 | elif command == "V": 97 | y = elements.pop() 98 | pos = current_pos.real + float(y) * 1j 99 | if not absolute: 100 | pos += current_pos.imag * 1j 101 | segments.append(path.Line(current_pos, pos)) 102 | current_pos = pos 103 | 104 | elif command == "C": 105 | control1 = float(elements.pop()) + float(elements.pop()) * 1j 106 | control2 = float(elements.pop()) + float(elements.pop()) * 1j 107 | end = float(elements.pop()) + float(elements.pop()) * 1j 108 | 109 | if not absolute: 110 | control1 += current_pos 111 | control2 += current_pos 112 | end += current_pos 113 | 114 | segments.append(path.CubicBezier(current_pos, control1, control2, end)) 115 | current_pos = end 116 | 117 | elif command == "S": 118 | # Smooth curve. First control point is the "reflection" of 119 | # the second control point in the previous path. 120 | 121 | if last_command not in "CS": 122 | # If there is no previous command or if the previous command 123 | # was not an C, c, S or s, assume the first control point is 124 | # coincident with the current point. 125 | control1 = current_pos 126 | else: 127 | # The first control point is assumed to be the reflection of 128 | # the second control point on the previous command relative 129 | # to the current point. 130 | control1 = current_pos + current_pos - segments[-1].control2 131 | 132 | control2 = float(elements.pop()) + float(elements.pop()) * 1j 133 | end = float(elements.pop()) + float(elements.pop()) * 1j 134 | 135 | if not absolute: 136 | control2 += current_pos 137 | end += current_pos 138 | 139 | segments.append(path.CubicBezier(current_pos, control1, control2, end)) 140 | current_pos = end 141 | 142 | elif command == "Q": 143 | control = float(elements.pop()) + float(elements.pop()) * 1j 144 | end = float(elements.pop()) + float(elements.pop()) * 1j 145 | 146 | if not absolute: 147 | control += current_pos 148 | end += current_pos 149 | 150 | segments.append(path.QuadraticBezier(current_pos, control, end)) 151 | current_pos = end 152 | 153 | elif command == "T": 154 | # Smooth curve. Control point is the "reflection" of 155 | # the second control point in the previous path. 156 | 157 | if last_command not in "QT": 158 | # If there is no previous command or if the previous command 159 | # was not an Q, q, T or t, assume the first control point is 160 | # coincident with the current point. 161 | control = current_pos 162 | else: 163 | # The control point is assumed to be the reflection of 164 | # the control point on the previous command relative 165 | # to the current point. 166 | control = current_pos + current_pos - segments[-1].control 167 | 168 | end = float(elements.pop()) + float(elements.pop()) * 1j 169 | 170 | if not absolute: 171 | end += current_pos 172 | 173 | segments.append(path.QuadraticBezier(current_pos, control, end)) 174 | current_pos = end 175 | 176 | elif command == "A": 177 | radius = float(elements.pop()) + float(elements.pop()) * 1j 178 | rotation = float(elements.pop()) 179 | arc = float(elements.pop()) 180 | sweep = float(elements.pop()) 181 | end = float(elements.pop()) + float(elements.pop()) * 1j 182 | 183 | if not absolute: 184 | end += current_pos 185 | 186 | segments.append(path.Arc(current_pos, radius, rotation, arc, sweep, end)) 187 | current_pos = end 188 | 189 | return segments 190 | -------------------------------------------------------------------------------- /python3.7libs/svg/path/path.py: -------------------------------------------------------------------------------- 1 | from __future__ import division 2 | from math import sqrt, cos, sin, acos, degrees, radians, log, pi 3 | from bisect import bisect 4 | 5 | try: 6 | from collections.abc import MutableSequence 7 | except ImportError: 8 | from collections import MutableSequence 9 | 10 | # This file contains classes for the different types of SVG path segments as 11 | # well as a Path object that contains a sequence of path segments. 12 | 13 | MIN_DEPTH = 5 14 | ERROR = 1e-12 15 | 16 | 17 | def segment_length(curve, start, end, start_point, end_point, error, min_depth, depth): 18 | """Recursively approximates the length by straight lines""" 19 | mid = (start + end) / 2 20 | mid_point = curve.point(mid) 21 | length = abs(end_point - start_point) 22 | first_half = abs(mid_point - start_point) 23 | second_half = abs(end_point - mid_point) 24 | 25 | length2 = first_half + second_half 26 | if (length2 - length > error) or (depth < min_depth): 27 | # Calculate the length of each segment: 28 | depth += 1 29 | return segment_length( 30 | curve, start, mid, start_point, mid_point, error, min_depth, depth 31 | ) + segment_length( 32 | curve, mid, end, mid_point, end_point, error, min_depth, depth 33 | ) 34 | # This is accurate enough. 35 | return length2 36 | 37 | 38 | class Linear(object): 39 | """A straight line 40 | 41 | The base for Line() and Close(). 42 | """ 43 | 44 | def __init__(self, start, end): 45 | self.start = start 46 | self.end = end 47 | 48 | def __ne__(self, other): 49 | if not isinstance(other, Line): 50 | return NotImplemented 51 | return not self == other 52 | 53 | def point(self, pos): 54 | distance = self.end - self.start 55 | return self.start + distance * pos 56 | 57 | def length(self, error=None, min_depth=None): 58 | distance = self.end - self.start 59 | return sqrt(distance.real ** 2 + distance.imag ** 2) 60 | 61 | 62 | class Line(Linear): 63 | def __repr__(self): 64 | return "Line(start=%s, end=%s)" % (self.start, self.end) 65 | 66 | def __eq__(self, other): 67 | if not isinstance(other, Line): 68 | return NotImplemented 69 | return self.start == other.start and self.end == other.end 70 | 71 | 72 | class CubicBezier(object): 73 | def __init__(self, start, control1, control2, end): 74 | self.start = start 75 | self.control1 = control1 76 | self.control2 = control2 77 | self.end = end 78 | 79 | def __repr__(self): 80 | return "CubicBezier(start=%s, control1=%s, control2=%s, end=%s)" % ( 81 | self.start, 82 | self.control1, 83 | self.control2, 84 | self.end, 85 | ) 86 | 87 | def __eq__(self, other): 88 | if not isinstance(other, CubicBezier): 89 | return NotImplemented 90 | return ( 91 | self.start == other.start 92 | and self.end == other.end 93 | and self.control1 == other.control1 94 | and self.control2 == other.control2 95 | ) 96 | 97 | def __ne__(self, other): 98 | if not isinstance(other, CubicBezier): 99 | return NotImplemented 100 | return not self == other 101 | 102 | def is_smooth_from(self, previous): 103 | """Checks if this segment would be a smooth segment following the previous""" 104 | if isinstance(previous, CubicBezier): 105 | return self.start == previous.end and (self.control1 - self.start) == ( 106 | previous.end - previous.control2 107 | ) 108 | else: 109 | return self.control1 == self.start 110 | 111 | def point(self, pos): 112 | """Calculate the x,y position at a certain position of the path""" 113 | return ( 114 | ((1 - pos) ** 3 * self.start) 115 | + (3 * (1 - pos) ** 2 * pos * self.control1) 116 | + (3 * (1 - pos) * pos ** 2 * self.control2) 117 | + (pos ** 3 * self.end) 118 | ) 119 | 120 | def length(self, error=ERROR, min_depth=MIN_DEPTH): 121 | """Calculate the length of the path up to a certain position""" 122 | start_point = self.point(0) 123 | end_point = self.point(1) 124 | return segment_length(self, 0, 1, start_point, end_point, error, min_depth, 0) 125 | 126 | 127 | class QuadraticBezier(object): 128 | def __init__(self, start, control, end): 129 | self.start = start 130 | self.end = end 131 | self.control = control 132 | 133 | def __repr__(self): 134 | return "QuadraticBezier(start=%s, control=%s, end=%s)" % ( 135 | self.start, 136 | self.control, 137 | self.end, 138 | ) 139 | 140 | def __eq__(self, other): 141 | if not isinstance(other, QuadraticBezier): 142 | return NotImplemented 143 | return ( 144 | self.start == other.start 145 | and self.end == other.end 146 | and self.control == other.control 147 | ) 148 | 149 | def __ne__(self, other): 150 | if not isinstance(other, QuadraticBezier): 151 | return NotImplemented 152 | return not self == other 153 | 154 | def is_smooth_from(self, previous): 155 | """Checks if this segment would be a smooth segment following the previous""" 156 | if isinstance(previous, QuadraticBezier): 157 | return self.start == previous.end and (self.control - self.start) == ( 158 | previous.end - previous.control 159 | ) 160 | else: 161 | return self.control == self.start 162 | 163 | def point(self, pos): 164 | return ( 165 | (1 - pos) ** 2 * self.start 166 | + 2 * (1 - pos) * pos * self.control 167 | + pos ** 2 * self.end 168 | ) 169 | 170 | def length(self, error=None, min_depth=None): 171 | a = self.start - 2 * self.control + self.end 172 | b = 2 * (self.control - self.start) 173 | a_dot_b = a.real * b.real + a.imag * b.imag 174 | 175 | if abs(a) < 1e-12: 176 | s = abs(b) 177 | elif abs(a_dot_b + abs(a) * abs(b)) < 1e-12: 178 | k = abs(b) / abs(a) 179 | if k >= 2: 180 | s = abs(b) - abs(a) 181 | else: 182 | s = abs(a) * (k ** 2 / 2 - k + 1) 183 | else: 184 | # For an explanation of this case, see 185 | # http://www.malczak.info/blog/quadratic-bezier-curve-length/ 186 | A = 4 * (a.real ** 2 + a.imag ** 2) 187 | B = 4 * (a.real * b.real + a.imag * b.imag) 188 | C = b.real ** 2 + b.imag ** 2 189 | 190 | Sabc = 2 * sqrt(A + B + C) 191 | A2 = sqrt(A) 192 | A32 = 2 * A * A2 193 | C2 = 2 * sqrt(C) 194 | BA = B / A2 195 | 196 | s = ( 197 | A32 * Sabc 198 | + A2 * B * (Sabc - C2) 199 | + (4 * C * A - B ** 2) * log((2 * A2 + BA + Sabc) / (BA + C2)) 200 | ) / (4 * A32) 201 | return s 202 | 203 | 204 | class Arc(object): 205 | def __init__(self, start, radius, rotation, arc, sweep, end): 206 | """radius is complex, rotation is in degrees, 207 | large and sweep are 1 or 0 (True/False also work)""" 208 | 209 | self.start = start 210 | self.radius = radius 211 | self.rotation = rotation 212 | self.arc = bool(arc) 213 | self.sweep = bool(sweep) 214 | self.end = end 215 | 216 | self._parameterize() 217 | 218 | def __repr__(self): 219 | return "Arc(start=%s, radius=%s, rotation=%s, arc=%s, sweep=%s, end=%s)" % ( 220 | self.start, 221 | self.radius, 222 | self.rotation, 223 | self.arc, 224 | self.sweep, 225 | self.end, 226 | ) 227 | 228 | def __eq__(self, other): 229 | if not isinstance(other, Arc): 230 | return NotImplemented 231 | return ( 232 | self.start == other.start 233 | and self.end == other.end 234 | and self.radius == other.radius 235 | and self.rotation == other.rotation 236 | and self.arc == other.arc 237 | and self.sweep == other.sweep 238 | ) 239 | 240 | def __ne__(self, other): 241 | if not isinstance(other, Arc): 242 | return NotImplemented 243 | return not self == other 244 | 245 | def _parameterize(self): 246 | # Conversion from endpoint to center parameterization 247 | # http://www.w3.org/TR/SVG/implnote.html#ArcImplementationNotes 248 | if self.start == self.end: 249 | # This is equivalent of omitting the segment, so do nothing 250 | return 251 | 252 | if self.radius.real == 0 or self.radius.imag == 0: 253 | # This should be treated as a straight line 254 | return 255 | 256 | cosr = cos(radians(self.rotation)) 257 | sinr = sin(radians(self.rotation)) 258 | dx = (self.start.real - self.end.real) / 2 259 | dy = (self.start.imag - self.end.imag) / 2 260 | x1prim = cosr * dx + sinr * dy 261 | x1prim_sq = x1prim * x1prim 262 | y1prim = -sinr * dx + cosr * dy 263 | y1prim_sq = y1prim * y1prim 264 | 265 | rx = self.radius.real 266 | rx_sq = rx * rx 267 | ry = self.radius.imag 268 | ry_sq = ry * ry 269 | 270 | # Correct out of range radii 271 | radius_scale = (x1prim_sq / rx_sq) + (y1prim_sq / ry_sq) 272 | if radius_scale > 1: 273 | radius_scale = sqrt(radius_scale) 274 | rx *= radius_scale 275 | ry *= radius_scale 276 | rx_sq = rx * rx 277 | ry_sq = ry * ry 278 | self.radius_scale = radius_scale 279 | else: 280 | # SVG spec only scales UP 281 | self.radius_scale = 1 282 | 283 | t1 = rx_sq * y1prim_sq 284 | t2 = ry_sq * x1prim_sq 285 | c = sqrt(abs((rx_sq * ry_sq - t1 - t2) / (t1 + t2))) 286 | 287 | if self.arc == self.sweep: 288 | c = -c 289 | cxprim = c * rx * y1prim / ry 290 | cyprim = -c * ry * x1prim / rx 291 | 292 | self.center = complex( 293 | (cosr * cxprim - sinr * cyprim) + ((self.start.real + self.end.real) / 2), 294 | (sinr * cxprim + cosr * cyprim) + ((self.start.imag + self.end.imag) / 2), 295 | ) 296 | 297 | ux = (x1prim - cxprim) / rx 298 | uy = (y1prim - cyprim) / ry 299 | vx = (-x1prim - cxprim) / rx 300 | vy = (-y1prim - cyprim) / ry 301 | n = sqrt(ux * ux + uy * uy) 302 | p = ux 303 | theta = degrees(acos(p / n)) 304 | if uy < 0: 305 | theta = -theta 306 | self.theta = theta % 360 307 | 308 | n = sqrt((ux * ux + uy * uy) * (vx * vx + vy * vy)) 309 | p = ux * vx + uy * vy 310 | d = p / n 311 | # In certain cases the above calculation can through inaccuracies 312 | # become just slightly out of range, f ex -1.0000000000000002. 313 | if d > 1.0: 314 | d = 1.0 315 | elif d < -1.0: 316 | d = -1.0 317 | delta = degrees(acos(d)) 318 | if (ux * vy - uy * vx) < 0: 319 | delta = -delta 320 | self.delta = delta % 360 321 | if not self.sweep: 322 | self.delta -= 360 323 | 324 | def point(self, pos): 325 | if self.start == self.end: 326 | # This is equivalent of omitting the segment 327 | return self.start 328 | 329 | if self.radius.real == 0 or self.radius.imag == 0: 330 | # This should be treated as a straight line 331 | distance = self.end - self.start 332 | return self.start + distance * pos 333 | 334 | angle = radians(self.theta + (self.delta * pos)) 335 | cosr = cos(radians(self.rotation)) 336 | sinr = sin(radians(self.rotation)) 337 | radius = self.radius * self.radius_scale 338 | 339 | x = ( 340 | cosr * cos(angle) * radius.real 341 | - sinr * sin(angle) * radius.imag 342 | + self.center.real 343 | ) 344 | y = ( 345 | sinr * cos(angle) * radius.real 346 | + cosr * sin(angle) * radius.imag 347 | + self.center.imag 348 | ) 349 | return complex(x, y) 350 | 351 | def length(self, error=ERROR, min_depth=MIN_DEPTH): 352 | """The length of an elliptical arc segment requires numerical 353 | integration, and in that case it's simpler to just do a geometric 354 | approximation, as for cubic bezier curves. 355 | """ 356 | if self.start == self.end: 357 | # This is equivalent of omitting the segment 358 | return 0 359 | 360 | if self.radius.real == 0 or self.radius.imag == 0: 361 | # This should be treated as a straight line 362 | distance = self.end - self.start 363 | return sqrt(distance.real ** 2 + distance.imag ** 2) 364 | 365 | if self.radius.real == self.radius.imag: 366 | # It's a circle, which simplifies this a LOT. 367 | radius = self.radius.real * self.radius_scale 368 | return abs(radius * self.delta * pi / 180) 369 | 370 | start_point = self.point(0) 371 | end_point = self.point(1) 372 | return segment_length(self, 0, 1, start_point, end_point, error, min_depth, 0) 373 | 374 | 375 | class Move(object): 376 | """Represents move commands. Does nothing, but is there to handle 377 | paths that consist of only move commands, which is valid, but pointless. 378 | """ 379 | 380 | def __init__(self, to): 381 | self.start = self.end = to 382 | 383 | def __repr__(self): 384 | return "Move(to=%s)" % self.start 385 | 386 | def __eq__(self, other): 387 | if not isinstance(other, Move): 388 | return NotImplemented 389 | return self.start == other.start 390 | 391 | def __ne__(self, other): 392 | if not isinstance(other, Move): 393 | return NotImplemented 394 | return not self == other 395 | 396 | def point(self, pos): 397 | return self.start 398 | 399 | def length(self, error=ERROR, min_depth=MIN_DEPTH): 400 | return 0 401 | 402 | 403 | class Close(Linear): 404 | """Represents the closepath command""" 405 | 406 | def __eq__(self, other): 407 | if not isinstance(other, Close): 408 | return NotImplemented 409 | return self.start == other.start and self.end == other.end 410 | 411 | def __repr__(self): 412 | return "Close(start=%s, end=%s)" % (self.start, self.end) 413 | 414 | 415 | class Path(MutableSequence): 416 | """A Path is a sequence of path segments""" 417 | 418 | def __init__(self, *segments): 419 | self._segments = list(segments) 420 | self._length = None 421 | self._lengths = None 422 | # Fractional distance from starting point through the end of each segment. 423 | self._fractions = [] 424 | 425 | def __getitem__(self, index): 426 | return self._segments[index] 427 | 428 | def __setitem__(self, index, value): 429 | self._segments[index] = value 430 | self._length = None 431 | 432 | def __delitem__(self, index): 433 | del self._segments[index] 434 | self._length = None 435 | 436 | def insert(self, index, value): 437 | self._segments.insert(index, value) 438 | self._length = None 439 | 440 | def reverse(self): 441 | # Reversing the order of a path would require reversing each element 442 | # as well. That's not implemented. 443 | raise NotImplementedError 444 | 445 | def __len__(self): 446 | return len(self._segments) 447 | 448 | def __repr__(self): 449 | return "Path(%s)" % (", ".join(repr(x) for x in self._segments)) 450 | 451 | def __eq__(self, other): 452 | 453 | if not isinstance(other, Path): 454 | return NotImplemented 455 | if len(self) != len(other): 456 | return False 457 | for s, o in zip(self._segments, other._segments): 458 | if not s == o: 459 | return False 460 | return True 461 | 462 | def __ne__(self, other): 463 | if not isinstance(other, Path): 464 | return NotImplemented 465 | return not self == other 466 | 467 | def _calc_lengths(self, error=ERROR, min_depth=MIN_DEPTH): 468 | if self._length is not None: 469 | return 470 | 471 | lengths = [ 472 | each.length(error=error, min_depth=min_depth) for each in self._segments 473 | ] 474 | self._length = sum(lengths) 475 | self._lengths = [each / self._length for each in lengths] 476 | # Calculate the fractional distance for each segment to use in point() 477 | fraction = 0 478 | for each in self._lengths: 479 | fraction += each 480 | self._fractions.append(fraction) 481 | 482 | def point(self, pos, error=ERROR): 483 | 484 | # Shortcuts 485 | if pos == 0.0: 486 | return self._segments[0].point(pos) 487 | if pos == 1.0: 488 | return self._segments[-1].point(pos) 489 | 490 | self._calc_lengths(error=error) 491 | # Find which segment the point we search for is located on: 492 | i = bisect(self._fractions, pos) 493 | if i == 0: 494 | segment_pos = pos / self._fractions[0] 495 | else: 496 | segment_pos = (pos - self._fractions[i - 1]) / ( 497 | self._fractions[i] - self._fractions[i - 1] 498 | ) 499 | return self._segments[i].point(segment_pos) 500 | 501 | def length(self, error=ERROR, min_depth=MIN_DEPTH): 502 | self._calc_lengths(error, min_depth) 503 | return self._length 504 | 505 | def d(self): 506 | current_pos = None 507 | parts = [] 508 | previous_segment = None 509 | end = self[-1].end 510 | 511 | for segment in self: 512 | start = segment.start 513 | # If the start of this segment does not coincide with the end of 514 | # the last segment or if this segment is actually the close point 515 | # of a closed path, then we should start a new subpath here. 516 | if isinstance(segment, Close): 517 | parts.append("Z") 518 | elif ( 519 | isinstance(segment, Move) 520 | or (current_pos != start) 521 | or (start == end and not isinstance(previous_segment, Move)) 522 | ): 523 | parts.append("M {0:G},{1:G}".format(start.real, start.imag)) 524 | 525 | if isinstance(segment, Line): 526 | parts.append("L {0:G},{1:G}".format(segment.end.real, segment.end.imag)) 527 | elif isinstance(segment, CubicBezier): 528 | if segment.is_smooth_from(previous_segment): 529 | parts.append( 530 | "S {0:G},{1:G} {2:G},{3:G}".format( 531 | segment.control2.real, 532 | segment.control2.imag, 533 | segment.end.real, 534 | segment.end.imag, 535 | ) 536 | ) 537 | else: 538 | parts.append( 539 | "C {0:G},{1:G} {2:G},{3:G} {4:G},{5:G}".format( 540 | segment.control1.real, 541 | segment.control1.imag, 542 | segment.control2.real, 543 | segment.control2.imag, 544 | segment.end.real, 545 | segment.end.imag, 546 | ) 547 | ) 548 | elif isinstance(segment, QuadraticBezier): 549 | if segment.is_smooth_from(previous_segment): 550 | parts.append( 551 | "T {0:G},{1:G}".format(segment.end.real, segment.end.imag) 552 | ) 553 | else: 554 | parts.append( 555 | "Q {0:G},{1:G} {2:G},{3:G}".format( 556 | segment.control.real, 557 | segment.control.imag, 558 | segment.end.real, 559 | segment.end.imag, 560 | ) 561 | ) 562 | elif isinstance(segment, Arc): 563 | parts.append( 564 | "A {0:G},{1:G} {2:G} {3:d},{4:d} {5:G},{6:G}".format( 565 | segment.radius.real, 566 | segment.radius.imag, 567 | segment.rotation, 568 | int(segment.arc), 569 | int(segment.sweep), 570 | segment.end.real, 571 | segment.end.imag, 572 | ) 573 | ) 574 | 575 | current_pos = segment.end 576 | previous_segment = segment 577 | 578 | return " ".join(parts) 579 | -------------------------------------------------------------------------------- /python3.7libs/tinycss/__init__.py: -------------------------------------------------------------------------------- 1 | # coding: utf-8 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 | from .version import VERSION 13 | 14 | from .css21 import CSS21Parser 15 | from .page3 import CSSPage3Parser 16 | from .fonts3 import CSSFonts3Parser 17 | 18 | 19 | __version__ = VERSION 20 | 21 | PARSER_MODULES = { 22 | 'page3': CSSPage3Parser, 23 | 'fonts3': CSSFonts3Parser, 24 | } 25 | 26 | 27 | def make_parser(*features, **kwargs): 28 | """Make a parser object with the chosen features. 29 | 30 | :param features: 31 | Positional arguments are base classes the new parser class will extend. 32 | The string ``'page3'`` is accepted as short for 33 | :class:`~page3.CSSPage3Parser`. 34 | The string ``'fonts3'`` is accepted as short for 35 | :class:`~fonts3.CSSFonts3Parser`. 36 | :param kwargs: 37 | Keyword arguments are passed to the parser’s constructor. 38 | :returns: 39 | An instance of a new subclass of :class:`CSS21Parser` 40 | 41 | """ 42 | if features: 43 | bases = tuple(PARSER_MODULES.get(f, f) for f in features) 44 | parser_class = type('CustomCSSParser', bases + (CSS21Parser,), {}) 45 | else: 46 | parser_class = CSS21Parser 47 | return parser_class(**kwargs) 48 | -------------------------------------------------------------------------------- /python3.7libs/tinycss/color3.py: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | """ 3 | tinycss.colors3 4 | --------------- 5 | 6 | Parser for CSS 3 color values 7 | http://www.w3.org/TR/css3-color/ 8 | 9 | This module does not provide anything that integrates in a parser class, 10 | only functions that parse single tokens from (eg.) a property value. 11 | 12 | :copyright: (c) 2012 by Simon Sapin. 13 | :license: BSD, see LICENSE for more details. 14 | """ 15 | 16 | from __future__ import division, unicode_literals 17 | 18 | import collections 19 | import itertools 20 | import re 21 | 22 | from .tokenizer import tokenize_grouped 23 | 24 | 25 | class RGBA(collections.namedtuple('RGBA', ['red', 'green', 'blue', 'alpha'])): 26 | """An RGBA color. 27 | 28 | A tuple of four floats in the 0..1 range: ``(r, g, b, a)``. 29 | Also has ``red``, ``green``, ``blue`` and ``alpha`` attributes to access 30 | the same values. 31 | 32 | """ 33 | 34 | 35 | def parse_color_string(css_string): 36 | """Parse a CSS string as a color value. 37 | 38 | This is a convenience wrapper around :func:`parse_color` in case you 39 | have a string that is not from a CSS stylesheet. 40 | 41 | :param css_string: 42 | An unicode string in CSS syntax. 43 | :returns: 44 | Same as :func:`parse_color`. 45 | 46 | """ 47 | tokens = list(tokenize_grouped(css_string.strip())) 48 | if len(tokens) == 1: 49 | return parse_color(tokens[0]) 50 | 51 | 52 | def parse_color(token): 53 | """Parse single token as a color value. 54 | 55 | :param token: 56 | A single :class:`~.token_data.Token` or 57 | :class:`~.token_data.ContainerToken`, as found eg. in a 58 | property value. 59 | :returns: 60 | * ``None``, if the token is not a valid CSS 3 color value. 61 | (No exception is raised.) 62 | * For the *currentColor* keyword: the string ``'currentColor'`` 63 | * Every other values (including keywords, HSL and HSLA) is converted 64 | to RGBA and returned as an :class:`RGBA` object (a 4-tuple with 65 | attribute access). 66 | The alpha channel is clipped to [0, 1], but R, G, or B can be 67 | out of range (eg. ``rgb(-51, 306, 0)`` is represented as 68 | ``(-.2, 1.2, 0, 1)``.) 69 | 70 | """ 71 | if token.type == 'IDENT': 72 | return COLOR_KEYWORDS.get(token.value.lower()) 73 | elif token.type == 'HASH': 74 | for multiplier, regexp in HASH_REGEXPS: 75 | match = regexp(token.value) 76 | if match: 77 | r, g, b = [int(group * multiplier, 16) / 255 78 | for group in match.groups()] 79 | return RGBA(r, g, b, 1.) 80 | elif token.type == 'FUNCTION': 81 | args = parse_comma_separated(token.content) 82 | if args: 83 | name = token.function_name.lower() 84 | if name == 'rgb': 85 | return parse_rgb(args, alpha=1.) 86 | elif name == 'rgba': 87 | alpha = parse_alpha(args[3:]) 88 | if alpha is not None: 89 | return parse_rgb(args[:3], alpha) 90 | elif name == 'hsl': 91 | return parse_hsl(args, alpha=1.) 92 | elif name == 'hsla': 93 | alpha = parse_alpha(args[3:]) 94 | if alpha is not None: 95 | return parse_hsl(args[:3], alpha) 96 | 97 | 98 | def parse_alpha(args): 99 | """ 100 | If args is a list of a single INTEGER or NUMBER token, 101 | retur its value clipped to the 0..1 range 102 | Otherwise, return None. 103 | """ 104 | if len(args) == 1 and args[0].type in ('NUMBER', 'INTEGER'): 105 | return min(1, max(0, args[0].value)) 106 | 107 | 108 | def parse_rgb(args, alpha): 109 | """ 110 | If args is a list of 3 INTEGER tokens or 3 PERCENTAGE tokens, 111 | return RGB values as a tuple of 3 floats in 0..1. 112 | Otherwise, return None. 113 | """ 114 | types = [arg.type for arg in args] 115 | if types == ['INTEGER', 'INTEGER', 'INTEGER']: 116 | r, g, b = [arg.value / 255 for arg in args[:3]] 117 | return RGBA(r, g, b, alpha) 118 | elif types == ['PERCENTAGE', 'PERCENTAGE', 'PERCENTAGE']: 119 | r, g, b = [arg.value / 100 for arg in args[:3]] 120 | return RGBA(r, g, b, alpha) 121 | 122 | 123 | def parse_hsl(args, alpha): 124 | """ 125 | If args is a list of 1 INTEGER token and 2 PERCENTAGE tokens, 126 | return RGB values as a tuple of 3 floats in 0..1. 127 | Otherwise, return None. 128 | """ 129 | types = [arg.type for arg in args] 130 | if types == ['INTEGER', 'PERCENTAGE', 'PERCENTAGE']: 131 | hsl = [arg.value for arg in args[:3]] 132 | r, g, b = hsl_to_rgb(*hsl) 133 | return RGBA(r, g, b, alpha) 134 | 135 | 136 | def hsl_to_rgb(hue, saturation, lightness): 137 | """ 138 | :param hue: degrees 139 | :param saturation: percentage 140 | :param lightness: percentage 141 | :returns: (r, g, b) as floats in the 0..1 range 142 | """ 143 | hue = (hue / 360) % 1 144 | saturation = min(1, max(0, saturation / 100)) 145 | lightness = min(1, max(0, lightness / 100)) 146 | 147 | # Translated from ABC: http://www.w3.org/TR/css3-color/#hsl-color 148 | def hue_to_rgb(m1, m2, h): 149 | if h < 0: 150 | h += 1 151 | if h > 1: 152 | h -= 1 153 | if h * 6 < 1: 154 | return m1 + (m2 - m1) * h * 6 155 | if h * 2 < 1: 156 | return m2 157 | if h * 3 < 2: 158 | return m1 + (m2 - m1) * (2 / 3 - h) * 6 159 | return m1 160 | 161 | if lightness <= 0.5: 162 | m2 = lightness * (saturation + 1) 163 | else: 164 | m2 = lightness + saturation - lightness * saturation 165 | m1 = lightness * 2 - m2 166 | return ( 167 | hue_to_rgb(m1, m2, hue + 1 / 3), 168 | hue_to_rgb(m1, m2, hue), 169 | hue_to_rgb(m1, m2, hue - 1 / 3), 170 | ) 171 | 172 | 173 | def parse_comma_separated(tokens): 174 | """Parse a list of tokens (typically the content of a function token) 175 | as arguments made of a single token each, separated by mandatory commas, 176 | with optional white space around each argument. 177 | 178 | return the argument list without commas or white space; 179 | or None if the function token content do not match the description above. 180 | 181 | """ 182 | tokens = [token for token in tokens if token.type != 'S'] 183 | if not tokens: 184 | return [] 185 | if len(tokens) % 2 == 1 and all( 186 | token.type == 'DELIM' and token.value == ',' 187 | for token in tokens[1::2]): 188 | return tokens[::2] 189 | 190 | 191 | HASH_REGEXPS = ( 192 | (2, re.compile('^#([\da-f])([\da-f])([\da-f])$', re.I).match), 193 | (1, re.compile('^#([\da-f]{2})([\da-f]{2})([\da-f]{2})$', re.I).match), 194 | ) 195 | 196 | 197 | # (r, g, b) in 0..255 198 | BASIC_COLOR_KEYWORDS = [ 199 | ('black', (0, 0, 0)), 200 | ('silver', (192, 192, 192)), 201 | ('gray', (128, 128, 128)), 202 | ('white', (255, 255, 255)), 203 | ('maroon', (128, 0, 0)), 204 | ('red', (255, 0, 0)), 205 | ('purple', (128, 0, 128)), 206 | ('fuchsia', (255, 0, 255)), 207 | ('green', (0, 128, 0)), 208 | ('lime', (0, 255, 0)), 209 | ('olive', (128, 128, 0)), 210 | ('yellow', (255, 255, 0)), 211 | ('navy', (0, 0, 128)), 212 | ('blue', (0, 0, 255)), 213 | ('teal', (0, 128, 128)), 214 | ('aqua', (0, 255, 255)), 215 | ] 216 | 217 | 218 | # (r, g, b) in 0..255 219 | EXTENDED_COLOR_KEYWORDS = [ 220 | ('aliceblue', (240, 248, 255)), 221 | ('antiquewhite', (250, 235, 215)), 222 | ('aqua', (0, 255, 255)), 223 | ('aquamarine', (127, 255, 212)), 224 | ('azure', (240, 255, 255)), 225 | ('beige', (245, 245, 220)), 226 | ('bisque', (255, 228, 196)), 227 | ('black', (0, 0, 0)), 228 | ('blanchedalmond', (255, 235, 205)), 229 | ('blue', (0, 0, 255)), 230 | ('blueviolet', (138, 43, 226)), 231 | ('brown', (165, 42, 42)), 232 | ('burlywood', (222, 184, 135)), 233 | ('cadetblue', (95, 158, 160)), 234 | ('chartreuse', (127, 255, 0)), 235 | ('chocolate', (210, 105, 30)), 236 | ('coral', (255, 127, 80)), 237 | ('cornflowerblue', (100, 149, 237)), 238 | ('cornsilk', (255, 248, 220)), 239 | ('crimson', (220, 20, 60)), 240 | ('cyan', (0, 255, 255)), 241 | ('darkblue', (0, 0, 139)), 242 | ('darkcyan', (0, 139, 139)), 243 | ('darkgoldenrod', (184, 134, 11)), 244 | ('darkgray', (169, 169, 169)), 245 | ('darkgreen', (0, 100, 0)), 246 | ('darkgrey', (169, 169, 169)), 247 | ('darkkhaki', (189, 183, 107)), 248 | ('darkmagenta', (139, 0, 139)), 249 | ('darkolivegreen', (85, 107, 47)), 250 | ('darkorange', (255, 140, 0)), 251 | ('darkorchid', (153, 50, 204)), 252 | ('darkred', (139, 0, 0)), 253 | ('darksalmon', (233, 150, 122)), 254 | ('darkseagreen', (143, 188, 143)), 255 | ('darkslateblue', (72, 61, 139)), 256 | ('darkslategray', (47, 79, 79)), 257 | ('darkslategrey', (47, 79, 79)), 258 | ('darkturquoise', (0, 206, 209)), 259 | ('darkviolet', (148, 0, 211)), 260 | ('deeppink', (255, 20, 147)), 261 | ('deepskyblue', (0, 191, 255)), 262 | ('dimgray', (105, 105, 105)), 263 | ('dimgrey', (105, 105, 105)), 264 | ('dodgerblue', (30, 144, 255)), 265 | ('firebrick', (178, 34, 34)), 266 | ('floralwhite', (255, 250, 240)), 267 | ('forestgreen', (34, 139, 34)), 268 | ('fuchsia', (255, 0, 255)), 269 | ('gainsboro', (220, 220, 220)), 270 | ('ghostwhite', (248, 248, 255)), 271 | ('gold', (255, 215, 0)), 272 | ('goldenrod', (218, 165, 32)), 273 | ('gray', (128, 128, 128)), 274 | ('green', (0, 128, 0)), 275 | ('greenyellow', (173, 255, 47)), 276 | ('grey', (128, 128, 128)), 277 | ('honeydew', (240, 255, 240)), 278 | ('hotpink', (255, 105, 180)), 279 | ('indianred', (205, 92, 92)), 280 | ('indigo', (75, 0, 130)), 281 | ('ivory', (255, 255, 240)), 282 | ('khaki', (240, 230, 140)), 283 | ('lavender', (230, 230, 250)), 284 | ('lavenderblush', (255, 240, 245)), 285 | ('lawngreen', (124, 252, 0)), 286 | ('lemonchiffon', (255, 250, 205)), 287 | ('lightblue', (173, 216, 230)), 288 | ('lightcoral', (240, 128, 128)), 289 | ('lightcyan', (224, 255, 255)), 290 | ('lightgoldenrodyellow', (250, 250, 210)), 291 | ('lightgray', (211, 211, 211)), 292 | ('lightgreen', (144, 238, 144)), 293 | ('lightgrey', (211, 211, 211)), 294 | ('lightpink', (255, 182, 193)), 295 | ('lightsalmon', (255, 160, 122)), 296 | ('lightseagreen', (32, 178, 170)), 297 | ('lightskyblue', (135, 206, 250)), 298 | ('lightslategray', (119, 136, 153)), 299 | ('lightslategrey', (119, 136, 153)), 300 | ('lightsteelblue', (176, 196, 222)), 301 | ('lightyellow', (255, 255, 224)), 302 | ('lime', (0, 255, 0)), 303 | ('limegreen', (50, 205, 50)), 304 | ('linen', (250, 240, 230)), 305 | ('magenta', (255, 0, 255)), 306 | ('maroon', (128, 0, 0)), 307 | ('mediumaquamarine', (102, 205, 170)), 308 | ('mediumblue', (0, 0, 205)), 309 | ('mediumorchid', (186, 85, 211)), 310 | ('mediumpurple', (147, 112, 219)), 311 | ('mediumseagreen', (60, 179, 113)), 312 | ('mediumslateblue', (123, 104, 238)), 313 | ('mediumspringgreen', (0, 250, 154)), 314 | ('mediumturquoise', (72, 209, 204)), 315 | ('mediumvioletred', (199, 21, 133)), 316 | ('midnightblue', (25, 25, 112)), 317 | ('mintcream', (245, 255, 250)), 318 | ('mistyrose', (255, 228, 225)), 319 | ('moccasin', (255, 228, 181)), 320 | ('navajowhite', (255, 222, 173)), 321 | ('navy', (0, 0, 128)), 322 | ('oldlace', (253, 245, 230)), 323 | ('olive', (128, 128, 0)), 324 | ('olivedrab', (107, 142, 35)), 325 | ('orange', (255, 165, 0)), 326 | ('orangered', (255, 69, 0)), 327 | ('orchid', (218, 112, 214)), 328 | ('palegoldenrod', (238, 232, 170)), 329 | ('palegreen', (152, 251, 152)), 330 | ('paleturquoise', (175, 238, 238)), 331 | ('palevioletred', (219, 112, 147)), 332 | ('papayawhip', (255, 239, 213)), 333 | ('peachpuff', (255, 218, 185)), 334 | ('peru', (205, 133, 63)), 335 | ('pink', (255, 192, 203)), 336 | ('plum', (221, 160, 221)), 337 | ('powderblue', (176, 224, 230)), 338 | ('purple', (128, 0, 128)), 339 | ('red', (255, 0, 0)), 340 | ('rosybrown', (188, 143, 143)), 341 | ('royalblue', (65, 105, 225)), 342 | ('saddlebrown', (139, 69, 19)), 343 | ('salmon', (250, 128, 114)), 344 | ('sandybrown', (244, 164, 96)), 345 | ('seagreen', (46, 139, 87)), 346 | ('seashell', (255, 245, 238)), 347 | ('sienna', (160, 82, 45)), 348 | ('silver', (192, 192, 192)), 349 | ('skyblue', (135, 206, 235)), 350 | ('slateblue', (106, 90, 205)), 351 | ('slategray', (112, 128, 144)), 352 | ('slategrey', (112, 128, 144)), 353 | ('snow', (255, 250, 250)), 354 | ('springgreen', (0, 255, 127)), 355 | ('steelblue', (70, 130, 180)), 356 | ('tan', (210, 180, 140)), 357 | ('teal', (0, 128, 128)), 358 | ('thistle', (216, 191, 216)), 359 | ('tomato', (255, 99, 71)), 360 | ('turquoise', (64, 224, 208)), 361 | ('violet', (238, 130, 238)), 362 | ('wheat', (245, 222, 179)), 363 | ('white', (255, 255, 255)), 364 | ('whitesmoke', (245, 245, 245)), 365 | ('yellow', (255, 255, 0)), 366 | ('yellowgreen', (154, 205, 50)), 367 | ] 368 | 369 | 370 | # (r, g, b, a) in 0..1 or a string marker 371 | SPECIAL_COLOR_KEYWORDS = { 372 | 'currentcolor': 'currentColor', 373 | 'transparent': RGBA(0., 0., 0., 0.), 374 | } 375 | 376 | 377 | # RGBA namedtuples of (r, g, b, a) in 0..1 or a string marker 378 | COLOR_KEYWORDS = SPECIAL_COLOR_KEYWORDS.copy() 379 | COLOR_KEYWORDS.update( 380 | # 255 maps to 1, 0 to 0, the rest is linear. 381 | (keyword, RGBA(r / 255., g / 255., b / 255., 1.)) 382 | for keyword, (r, g, b) in itertools.chain( 383 | BASIC_COLOR_KEYWORDS, EXTENDED_COLOR_KEYWORDS)) 384 | -------------------------------------------------------------------------------- /python3.7libs/tinycss/css21.py: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | """ 3 | tinycss.css21 4 | ------------- 5 | 6 | Parser for CSS 2.1 7 | http://www.w3.org/TR/CSS21/syndata.html 8 | 9 | :copyright: (c) 2012 by Simon Sapin. 10 | :license: BSD, see LICENSE for more details. 11 | """ 12 | 13 | from __future__ import unicode_literals 14 | 15 | from itertools import chain, islice 16 | 17 | from .decoding import decode 18 | from .parsing import ( 19 | ParseError, remove_whitespace, split_on_comma, strip_whitespace, 20 | validate_any, validate_value) 21 | from .token_data import TokenList 22 | from .tokenizer import tokenize_grouped 23 | 24 | 25 | # stylesheet : [ CDO | CDC | S | statement ]*; 26 | # statement : ruleset | at-rule; 27 | # at-rule : ATKEYWORD S* any* [ block | ';' S* ]; 28 | # block : '{' S* [ any | block | ATKEYWORD S* | ';' S* ]* '}' S*; 29 | # ruleset : selector? '{' S* declaration? [ ';' S* declaration? ]* '}' S*; 30 | # selector : any+; 31 | # declaration : property S* ':' S* value; 32 | # property : IDENT; 33 | # value : [ any | block | ATKEYWORD S* ]+; 34 | # any : [ IDENT | NUMBER | PERCENTAGE | DIMENSION | STRING 35 | # | DELIM | URI | HASH | UNICODE-RANGE | INCLUDES 36 | # | DASHMATCH | ':' | FUNCTION S* [any|unused]* ')' 37 | # | '(' S* [any|unused]* ')' | '[' S* [any|unused]* ']' 38 | # ] S*; 39 | # unused : block | ATKEYWORD S* | ';' S* | CDO S* | CDC S*; 40 | 41 | 42 | class Stylesheet(object): 43 | """ 44 | A parsed CSS stylesheet. 45 | 46 | .. attribute:: rules 47 | 48 | A mixed list, in source order, of :class:`RuleSet` and various 49 | at-rules such as :class:`ImportRule`, :class:`MediaRule` 50 | and :class:`PageRule`. 51 | Use their :obj:`at_keyword` attribute to distinguish them. 52 | 53 | .. attribute:: errors 54 | 55 | A list of :class:`~.parsing.ParseError`. Invalid rules and declarations 56 | are ignored, with the details logged in this list. 57 | 58 | .. attribute:: encoding 59 | 60 | The character encoding that was used to decode the stylesheet 61 | from bytes, or ``None`` for Unicode stylesheets. 62 | 63 | """ 64 | def __init__(self, rules, errors, encoding): 65 | self.rules = rules 66 | self.errors = errors 67 | self.encoding = encoding 68 | 69 | def __repr__(self): 70 | return '<{0.__class__.__name__} {1} rules {2} errors>'.format( 71 | self, len(self.rules), len(self.errors)) 72 | 73 | 74 | class AtRule(object): 75 | """ 76 | An unparsed at-rule. 77 | 78 | .. attribute:: at_keyword 79 | 80 | The normalized (lower-case) at-keyword as a string. Eg: ``'@page'`` 81 | 82 | .. attribute:: head 83 | 84 | The part of the at-rule between the at-keyword and the ``{`` 85 | marking the body, or the ``;`` marking the end of an at-rule without 86 | a body. A :class:`~.token_data.TokenList`. 87 | 88 | .. attribute:: body 89 | 90 | The content of the body between ``{`` and ``}`` as a 91 | :class:`~.token_data.TokenList`, or ``None`` if there is no body 92 | (ie. if the rule ends with ``;``). 93 | 94 | The head was validated against the core grammar but **not** the body, 95 | as the body might contain declarations. In case of an error in a 96 | declaration, parsing should continue from the next declaration. 97 | The whole rule should not be ignored as it would be for an error 98 | in the head. 99 | 100 | These at-rules are expected to be parsed further before reaching 101 | the user API. 102 | 103 | """ 104 | def __init__(self, at_keyword, head, body, line, column): 105 | self.at_keyword = at_keyword 106 | self.head = TokenList(head) 107 | self.body = TokenList(body) if body is not None else body 108 | self.line = line 109 | self.column = column 110 | 111 | def __repr__(self): 112 | return ('<{0.__class__.__name__} {0.line}:{0.column} {0.at_keyword}>' 113 | .format(self)) 114 | 115 | 116 | class RuleSet(object): 117 | """A ruleset. 118 | 119 | .. attribute:: at_keyword 120 | 121 | Always ``None``. Helps to tell rulesets apart from at-rules. 122 | 123 | .. attribute:: selector 124 | 125 | The selector as a :class:`~.token_data.TokenList`. 126 | In CSS 3, this is actually called a selector group. 127 | 128 | ``rule.selector.as_css()`` gives the selector as a string. 129 | This string can be used with *cssselect*, see :ref:`selectors3`. 130 | 131 | .. attribute:: declarations 132 | 133 | The list of :class:`Declaration`, in source order. 134 | 135 | """ 136 | 137 | at_keyword = None 138 | 139 | def __init__(self, selector, declarations, line, column): 140 | self.selector = TokenList(selector) 141 | self.declarations = declarations 142 | self.line = line 143 | self.column = column 144 | 145 | def __repr__(self): 146 | return ('<{0.__class__.__name__} at {0.line}:{0.column} {1}>' 147 | .format(self, self.selector.as_css())) 148 | 149 | 150 | class Declaration(object): 151 | """A property declaration. 152 | 153 | .. attribute:: name 154 | 155 | The property name as a normalized (lower-case) string. 156 | 157 | .. attribute:: value 158 | 159 | The property value as a :class:`~.token_data.TokenList`. 160 | 161 | The value is not parsed. UAs using tinycss may only support 162 | some properties or some values and tinycss does not know which. 163 | They need to parse values themselves and ignore declarations with 164 | unknown or unsupported properties or values, and fall back 165 | on any previous declaration. 166 | 167 | :mod:`tinycss.color3` parses color values, but other values 168 | will need specific parsing/validation code. 169 | 170 | .. attribute:: priority 171 | 172 | Either the string ``'important'`` or ``None``. 173 | 174 | """ 175 | def __init__(self, name, value, priority, line, column): 176 | self.name = name 177 | self.value = TokenList(value) 178 | self.priority = priority 179 | self.line = line 180 | self.column = column 181 | 182 | def __repr__(self): 183 | priority = ' !' + self.priority if self.priority else '' 184 | return ('<{0.__class__.__name__} {0.line}:{0.column}' 185 | ' {0.name}: {1}{2}>'.format( 186 | self, self.value.as_css(), priority)) 187 | 188 | 189 | class PageRule(object): 190 | """A parsed CSS 2.1 @page rule. 191 | 192 | .. attribute:: at_keyword 193 | 194 | Always ``'@page'`` 195 | 196 | .. attribute:: selector 197 | 198 | The page selector. 199 | In CSS 2.1 this is either ``None`` (no selector), or the string 200 | ``'first'``, ``'left'`` or ``'right'`` for the pseudo class 201 | of the same name. 202 | 203 | .. attribute:: specificity 204 | 205 | Specificity of the page selector. This is a tuple of four integers, 206 | but these tuples are mostly meant to be compared to each other. 207 | 208 | .. attribute:: declarations 209 | 210 | A list of :class:`Declaration`, in source order. 211 | 212 | .. attribute:: at_rules 213 | 214 | The list of parsed at-rules inside the @page block, in source order. 215 | Always empty for CSS 2.1. 216 | 217 | """ 218 | at_keyword = '@page' 219 | 220 | def __init__(self, selector, specificity, declarations, at_rules, 221 | line, column): 222 | self.selector = selector 223 | self.specificity = specificity 224 | self.declarations = declarations 225 | self.at_rules = at_rules 226 | self.line = line 227 | self.column = column 228 | 229 | def __repr__(self): 230 | return ('<{0.__class__.__name__} {0.line}:{0.column}' 231 | ' {0.selector}>'.format(self)) 232 | 233 | 234 | class MediaRule(object): 235 | """A parsed @media rule. 236 | 237 | .. attribute:: at_keyword 238 | 239 | Always ``'@media'`` 240 | 241 | .. attribute:: media 242 | 243 | For CSS 2.1 without media queries: the media types 244 | as a list of strings. 245 | 246 | .. attribute:: rules 247 | 248 | The list :class:`RuleSet` and various at-rules inside the @media 249 | block, in source order. 250 | 251 | """ 252 | at_keyword = '@media' 253 | 254 | def __init__(self, media, rules, line, column): 255 | self.media = media 256 | self.rules = rules 257 | self.line = line 258 | self.column = column 259 | 260 | def __repr__(self): 261 | return ('<{0.__class__.__name__} {0.line}:{0.column}' 262 | ' {0.media}>'.format(self)) 263 | 264 | 265 | class ImportRule(object): 266 | """A parsed @import rule. 267 | 268 | .. attribute:: at_keyword 269 | 270 | Always ``'@import'`` 271 | 272 | .. attribute:: uri 273 | 274 | The URI to be imported, as read from the stylesheet. 275 | (URIs are not made absolute.) 276 | 277 | .. attribute:: media 278 | 279 | For CSS 2.1 without media queries: the media types 280 | as a list of strings. 281 | This attribute is explicitly ``['all']`` if the media was omitted 282 | in the source. 283 | 284 | """ 285 | at_keyword = '@import' 286 | 287 | def __init__(self, uri, media, line, column): 288 | self.uri = uri 289 | self.media = media 290 | self.line = line 291 | self.column = column 292 | 293 | def __repr__(self): 294 | return ('<{0.__class__.__name__} {0.line}:{0.column}' 295 | ' {0.uri}>'.format(self)) 296 | 297 | 298 | def _remove_at_charset(tokens): 299 | """Remove any valid @charset at the beggining of a token stream. 300 | 301 | :param tokens: 302 | An iterable of tokens 303 | :returns: 304 | A possibly truncated iterable of tokens 305 | 306 | """ 307 | tokens = iter(tokens) 308 | header = list(islice(tokens, 4)) 309 | if [t.type for t in header] == ['ATKEYWORD', 'S', 'STRING', ';']: 310 | atkw, space, string, semicolon = header 311 | if ((atkw.value, space.value) == ('@charset', ' ') and 312 | string.as_css()[0] == '"'): 313 | # Found a valid @charset rule, only keep what’s after it. 314 | return tokens 315 | return chain(header, tokens) 316 | 317 | 318 | class CSS21Parser(object): 319 | """Parser for CSS 2.1 320 | 321 | This parser supports the core CSS syntax as well as @import, @media, 322 | @page and !important. 323 | 324 | Note that property values are still not parsed, as UAs using this 325 | parser may only support some properties or some values. 326 | 327 | Currently the parser holds no state. It being a class only allows 328 | subclassing and overriding its methods. 329 | 330 | """ 331 | 332 | # User API: 333 | 334 | def parse_stylesheet_file(self, css_file, protocol_encoding=None, 335 | linking_encoding=None, document_encoding=None): 336 | """Parse a stylesheet from a file or filename. 337 | 338 | Character encoding-related parameters and behavior are the same 339 | as in :meth:`parse_stylesheet_bytes`. 340 | 341 | :param css_file: 342 | Either a file (any object with a :meth:`~file.read` method) 343 | or a filename. 344 | :return: 345 | A :class:`Stylesheet`. 346 | 347 | """ 348 | if hasattr(css_file, 'read'): 349 | css_bytes = css_file.read() 350 | else: 351 | with open(css_file, 'rb') as fd: 352 | css_bytes = fd.read() 353 | return self.parse_stylesheet_bytes(css_bytes, protocol_encoding, 354 | linking_encoding, document_encoding) 355 | 356 | def parse_stylesheet_bytes(self, css_bytes, protocol_encoding=None, 357 | linking_encoding=None, document_encoding=None): 358 | """Parse a stylesheet from a byte string. 359 | 360 | The character encoding is determined from the passed metadata and the 361 | ``@charset`` rule in the stylesheet (if any). 362 | If no encoding information is available or decoding fails, 363 | decoding defaults to UTF-8 and then fall back on ISO-8859-1. 364 | 365 | :param css_bytes: 366 | A CSS stylesheet as a byte string. 367 | :param protocol_encoding: 368 | The "charset" parameter of a "Content-Type" HTTP header (if any), 369 | or similar metadata for other protocols. 370 | :param linking_encoding: 371 | ```` or other metadata from the linking mechanism 372 | (if any) 373 | :param document_encoding: 374 | Encoding of the referring style sheet or document (if any) 375 | :return: 376 | A :class:`Stylesheet`. 377 | 378 | """ 379 | css_unicode, encoding = decode(css_bytes, protocol_encoding, 380 | linking_encoding, document_encoding) 381 | return self.parse_stylesheet(css_unicode, encoding=encoding) 382 | 383 | def parse_stylesheet(self, css_unicode, encoding=None): 384 | """Parse a stylesheet from an Unicode string. 385 | 386 | :param css_unicode: 387 | A CSS stylesheet as an unicode string. 388 | :param encoding: 389 | The character encoding used to decode the stylesheet from bytes, 390 | if any. 391 | :return: 392 | A :class:`Stylesheet`. 393 | 394 | """ 395 | tokens = tokenize_grouped(css_unicode) 396 | if encoding: 397 | tokens = _remove_at_charset(tokens) 398 | rules, errors = self.parse_rules(tokens, context='stylesheet') 399 | return Stylesheet(rules, errors, encoding) 400 | 401 | def parse_style_attr(self, css_source): 402 | """Parse a "style" attribute (eg. of an HTML element). 403 | 404 | This method only accepts Unicode as the source (HTML) document 405 | is supposed to handle the character encoding. 406 | 407 | :param css_source: 408 | The attribute value, as an unicode string. 409 | :return: 410 | A tuple of the list of valid :class:`Declaration` and 411 | a list of :class:`~.parsing.ParseError`. 412 | """ 413 | return self.parse_declaration_list(tokenize_grouped(css_source)) 414 | 415 | # API for subclasses: 416 | 417 | def parse_rules(self, tokens, context): 418 | """Parse a sequence of rules (rulesets and at-rules). 419 | 420 | :param tokens: 421 | An iterable of tokens. 422 | :param context: 423 | Either ``'stylesheet'`` or an at-keyword such as ``'@media'``. 424 | (Most at-rules are only allowed in some contexts.) 425 | :return: 426 | A tuple of a list of parsed rules and a list of 427 | :class:`~.parsing.ParseError`. 428 | 429 | """ 430 | rules = [] 431 | errors = [] 432 | tokens = iter(tokens) 433 | for token in tokens: 434 | if token.type not in ('S', 'CDO', 'CDC'): 435 | try: 436 | if token.type == 'ATKEYWORD': 437 | rule = self.read_at_rule(token, tokens) 438 | result = self.parse_at_rule( 439 | rule, rules, errors, context) 440 | rules.append(result) 441 | else: 442 | rule, rule_errors = self.parse_ruleset(token, tokens) 443 | rules.append(rule) 444 | errors.extend(rule_errors) 445 | except ParseError as exc: 446 | errors.append(exc) 447 | # Skip the entire rule 448 | return rules, errors 449 | 450 | def read_at_rule(self, at_keyword_token, tokens): 451 | """Read an at-rule from a token stream. 452 | 453 | :param at_keyword_token: 454 | The ATKEYWORD token that starts this at-rule 455 | You may have read it already to distinguish the rule 456 | from a ruleset. 457 | :param tokens: 458 | An iterator of subsequent tokens. Will be consumed just enough 459 | for one at-rule. 460 | :return: 461 | An unparsed :class:`AtRule`. 462 | :raises: 463 | :class:`~.parsing.ParseError` if the head is invalid for the core 464 | grammar. The body is **not** validated. See :class:`AtRule`. 465 | 466 | """ 467 | # CSS syntax is case-insensitive 468 | at_keyword = at_keyword_token.value.lower() 469 | head = [] 470 | # For the ParseError in case `tokens` is empty: 471 | token = at_keyword_token 472 | for token in tokens: 473 | if token.type in '{;': 474 | break 475 | # Ignore white space just after the at-keyword. 476 | else: 477 | head.append(token) 478 | # On unexpected end of stylesheet, pretend that a ';' was there 479 | head = strip_whitespace(head) 480 | for head_token in head: 481 | validate_any(head_token, 'at-rule head') 482 | body = token.content if token.type == '{' else None 483 | return AtRule(at_keyword, head, body, 484 | at_keyword_token.line, at_keyword_token.column) 485 | 486 | def parse_at_rule(self, rule, previous_rules, errors, context): 487 | """Parse an at-rule. 488 | 489 | Subclasses that override this method must use ``super()`` and 490 | pass its return value for at-rules they do not know. 491 | 492 | In CSS 2.1, this method handles @charset, @import, @media and @page 493 | rules. 494 | 495 | :param rule: 496 | An unparsed :class:`AtRule`. 497 | :param previous_rules: 498 | The list of at-rules and rulesets that have been parsed so far 499 | in this context. This list can be used to decide if the current 500 | rule is valid. (For example, @import rules are only allowed 501 | before anything but a @charset rule.) 502 | :param context: 503 | Either ``'stylesheet'`` or an at-keyword such as ``'@media'``. 504 | (Most at-rules are only allowed in some contexts.) 505 | :raises: 506 | :class:`~.parsing.ParseError` if the rule is invalid. 507 | :return: 508 | A parsed at-rule 509 | 510 | """ 511 | if rule.at_keyword == '@page': 512 | if context != 'stylesheet': 513 | raise ParseError(rule, '@page rule not allowed in ' + context) 514 | selector, specificity = self.parse_page_selector(rule.head) 515 | if rule.body is None: 516 | raise ParseError( 517 | rule, 'invalid {0} rule: missing block'.format( 518 | rule.at_keyword)) 519 | declarations, at_rules, rule_errors = \ 520 | self.parse_declarations_and_at_rules(rule.body, '@page') 521 | errors.extend(rule_errors) 522 | return PageRule(selector, specificity, declarations, at_rules, 523 | rule.line, rule.column) 524 | 525 | elif rule.at_keyword == '@media': 526 | if context != 'stylesheet': 527 | raise ParseError(rule, '@media rule not allowed in ' + context) 528 | if not rule.head: 529 | raise ParseError(rule, 'expected media types for @media') 530 | media = self.parse_media(rule.head) 531 | if rule.body is None: 532 | raise ParseError( 533 | rule, 'invalid {0} rule: missing block'.format( 534 | rule.at_keyword)) 535 | rules, rule_errors = self.parse_rules(rule.body, '@media') 536 | errors.extend(rule_errors) 537 | return MediaRule(media, rules, rule.line, rule.column) 538 | 539 | elif rule.at_keyword == '@import': 540 | if context != 'stylesheet': 541 | raise ParseError( 542 | rule, '@import rule not allowed in ' + context) 543 | for previous_rule in previous_rules: 544 | if previous_rule.at_keyword not in ('@charset', '@import'): 545 | if previous_rule.at_keyword: 546 | type_ = 'an {0} rule'.format(previous_rule.at_keyword) 547 | else: 548 | type_ = 'a ruleset' 549 | raise ParseError( 550 | previous_rule, 551 | '@import rule not allowed after ' + type_) 552 | head = rule.head 553 | if not head: 554 | raise ParseError( 555 | rule, 'expected URI or STRING for @import rule') 556 | if head[0].type not in ('URI', 'STRING'): 557 | raise ParseError( 558 | rule, 'expected URI or STRING for @import rule, got ' + 559 | head[0].type) 560 | uri = head[0].value 561 | media = self.parse_media(strip_whitespace(head[1:])) 562 | if rule.body is not None: 563 | # The position of the ';' token would be best, but we don’t 564 | # have it anymore here. 565 | raise ParseError(head[-1], "expected ';', got a block") 566 | return ImportRule(uri, media, rule.line, rule.column) 567 | 568 | elif rule.at_keyword == '@charset': 569 | raise ParseError(rule, 'mis-placed or malformed @charset rule') 570 | 571 | else: 572 | raise ParseError( 573 | rule, 'unknown at-rule in {0} context: {1}'.format( 574 | context, rule.at_keyword)) 575 | 576 | def parse_media(self, tokens): 577 | """For CSS 2.1, parse a list of media types. 578 | 579 | Media Queries are expected to override this. 580 | 581 | :param tokens: 582 | A list of tokens 583 | :raises: 584 | :class:`~.parsing.ParseError` on invalid media types/queries 585 | :returns: 586 | For CSS 2.1, a list of media types as strings 587 | """ 588 | if not tokens: 589 | return ['all'] 590 | media_types = [] 591 | for part in split_on_comma(remove_whitespace(tokens)): 592 | types = [token.type for token in part] 593 | if types == ['IDENT']: 594 | media_types.append(part[0].value) 595 | else: 596 | raise ParseError( 597 | tokens[0], 'expected a media type' + 598 | ((', got ' + ', '.join(types)) if types else '')) 599 | return media_types 600 | 601 | def parse_page_selector(self, tokens): 602 | """Parse an @page selector. 603 | 604 | :param tokens: 605 | An iterable of token, typically from the ``head`` attribute of 606 | an unparsed :class:`AtRule`. 607 | :returns: 608 | A page selector. For CSS 2.1, this is ``'first'``, ``'left'``, 609 | ``'right'`` or ``None``. 610 | :raises: 611 | :class:`~.parsing.ParseError` on invalid selectors 612 | 613 | """ 614 | if not tokens: 615 | return None, (0, 0) 616 | if (len(tokens) == 2 and tokens[0].type == ':' and 617 | tokens[1].type == 'IDENT'): 618 | pseudo_class = tokens[1].value 619 | specificity = { 620 | 'first': (1, 0), 'left': (0, 1), 'right': (0, 1), 621 | }.get(pseudo_class) 622 | if specificity: 623 | return pseudo_class, specificity 624 | raise ParseError(tokens[0], 'invalid @page selector') 625 | 626 | def parse_declarations_and_at_rules(self, tokens, context): 627 | """Parse a mixed list of declarations and at rules, as found eg. 628 | in the body of an @page rule. 629 | 630 | Note that to add supported at-rules inside @page, 631 | :class:`~.page3.CSSPage3Parser` extends :meth:`parse_at_rule`, 632 | not this method. 633 | 634 | :param tokens: 635 | An iterable of token, typically from the ``body`` attribute of 636 | an unparsed :class:`AtRule`. 637 | :param context: 638 | An at-keyword such as ``'@page'``. 639 | (Most at-rules are only allowed in some contexts.) 640 | :returns: 641 | A tuple of: 642 | 643 | * A list of :class:`Declaration` 644 | * A list of parsed at-rules (empty for CSS 2.1) 645 | * A list of :class:`~.parsing.ParseError` 646 | 647 | """ 648 | at_rules = [] 649 | declarations = [] 650 | errors = [] 651 | tokens = iter(tokens) 652 | for token in tokens: 653 | if token.type == 'ATKEYWORD': 654 | try: 655 | rule = self.read_at_rule(token, tokens) 656 | result = self.parse_at_rule( 657 | rule, at_rules, errors, context) 658 | at_rules.append(result) 659 | except ParseError as err: 660 | errors.append(err) 661 | elif token.type != 'S': 662 | declaration_tokens = [] 663 | while token and token.type != ';': 664 | declaration_tokens.append(token) 665 | token = next(tokens, None) 666 | if declaration_tokens: 667 | try: 668 | declarations.append( 669 | self.parse_declaration(declaration_tokens)) 670 | except ParseError as err: 671 | errors.append(err) 672 | return declarations, at_rules, errors 673 | 674 | def parse_ruleset(self, first_token, tokens): 675 | """Parse a ruleset: a selector followed by declaration block. 676 | 677 | :param first_token: 678 | The first token of the ruleset (probably of the selector). 679 | You may have read it already to distinguish the rule 680 | from an at-rule. 681 | :param tokens: 682 | an iterator of subsequent tokens. Will be consumed just enough 683 | for one ruleset. 684 | :return: 685 | a tuple of a :class:`RuleSet` and an error list. 686 | The errors are recovered :class:`~.parsing.ParseError` in 687 | declarations. (Parsing continues from the next declaration on such 688 | errors.) 689 | :raises: 690 | :class:`~.parsing.ParseError` if the selector is invalid for the 691 | core grammar. 692 | Note a that a selector can be valid for the core grammar but 693 | not for CSS 2.1 or another level. 694 | 695 | """ 696 | selector = [] 697 | for token in chain([first_token], tokens): 698 | if token.type == '{': 699 | # Parse/validate once we’ve read the whole rule 700 | selector = strip_whitespace(selector) 701 | if not selector: 702 | raise ParseError(first_token, 'empty selector') 703 | for selector_token in selector: 704 | validate_any(selector_token, 'selector') 705 | declarations, errors = self.parse_declaration_list( 706 | token.content) 707 | ruleset = RuleSet(selector, declarations, 708 | first_token.line, first_token.column) 709 | return ruleset, errors 710 | else: 711 | selector.append(token) 712 | raise ParseError(token, 'no declaration block found for ruleset') 713 | 714 | def parse_declaration_list(self, tokens): 715 | """Parse a ``;`` separated declaration list. 716 | 717 | You may want to use :meth:`parse_declarations_and_at_rules` (or 718 | some other method that uses :func:`parse_declaration` directly) 719 | instead if you have not just declarations in the same context. 720 | 721 | :param tokens: 722 | an iterable of tokens. Should stop at (before) the end 723 | of the block, as marked by ``}``. 724 | :return: 725 | a tuple of the list of valid :class:`Declaration` and a list 726 | of :class:`~.parsing.ParseError` 727 | 728 | """ 729 | # split at ';' 730 | parts = [] 731 | this_part = [] 732 | for token in tokens: 733 | if token.type == ';': 734 | parts.append(this_part) 735 | this_part = [] 736 | else: 737 | this_part.append(token) 738 | parts.append(this_part) 739 | 740 | declarations = [] 741 | errors = [] 742 | for tokens in parts: 743 | tokens = strip_whitespace(tokens) 744 | if tokens: 745 | try: 746 | declarations.append(self.parse_declaration(tokens)) 747 | except ParseError as exc: 748 | errors.append(exc) 749 | # Skip the entire declaration 750 | return declarations, errors 751 | 752 | def parse_declaration(self, tokens): 753 | """Parse a single declaration. 754 | 755 | :param tokens: 756 | an iterable of at least one token. Should stop at (before) 757 | the end of the declaration, as marked by a ``;`` or ``}``. 758 | Empty declarations (ie. consecutive ``;`` with only white space 759 | in-between) should be skipped earlier and not passed to 760 | this method. 761 | :returns: 762 | a :class:`Declaration` 763 | :raises: 764 | :class:`~.parsing.ParseError` if the tokens do not match the 765 | 'declaration' production of the core grammar. 766 | 767 | """ 768 | tokens = iter(tokens) 769 | 770 | name_token = next(tokens) # assume there is at least one 771 | if name_token.type == 'IDENT': 772 | # CSS syntax is case-insensitive 773 | property_name = name_token.value.lower() 774 | else: 775 | raise ParseError( 776 | name_token, 'expected a property name, got {0}'.format( 777 | name_token.type)) 778 | 779 | token = name_token # In case ``tokens`` is now empty 780 | for token in tokens: 781 | if token.type == ':': 782 | break 783 | elif token.type != 'S': 784 | raise ParseError( 785 | token, "expected ':', got {0}".format(token.type)) 786 | else: 787 | raise ParseError(token, "expected ':'") 788 | 789 | value = strip_whitespace(list(tokens)) 790 | if not value: 791 | raise ParseError(token, 'expected a property value') 792 | validate_value(value) 793 | value, priority = self.parse_value_priority(value) 794 | return Declaration( 795 | property_name, value, priority, name_token.line, name_token.column) 796 | 797 | def parse_value_priority(self, tokens): 798 | """Separate any ``!important`` marker at the end of a property value. 799 | 800 | :param tokens: 801 | A list of tokens for the property value. 802 | :returns: 803 | A tuple of the actual property value (a list of tokens) 804 | and the :attr:`~Declaration.priority`. 805 | """ 806 | value = list(tokens) 807 | # Walk the token list from the end 808 | token = value.pop() 809 | if token.type == 'IDENT' and token.value.lower() == 'important': 810 | while value: 811 | token = value.pop() 812 | if token.type == 'DELIM' and token.value == '!': 813 | # Skip any white space before the '!' 814 | while value and value[-1].type == 'S': 815 | value.pop() 816 | if not value: 817 | raise ParseError( 818 | token, 'expected a value before !important') 819 | return value, 'important' 820 | # Skip white space between '!' and 'important' 821 | elif token.type != 'S': 822 | break 823 | return tokens, None 824 | -------------------------------------------------------------------------------- /python3.7libs/tinycss/decoding.py: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | """ 3 | tinycss.decoding 4 | ---------------- 5 | 6 | Decoding stylesheets from bytes to Unicode. 7 | http://www.w3.org/TR/CSS21/syndata.html#charset 8 | 9 | :copyright: (c) 2012 by Simon Sapin. 10 | :license: BSD, see LICENSE for more details. 11 | """ 12 | 13 | from __future__ import unicode_literals 14 | 15 | import operator 16 | import re 17 | from binascii import unhexlify 18 | 19 | __all__ = ['decode'] # Everything else is implementation detail 20 | 21 | 22 | def decode(css_bytes, protocol_encoding=None, 23 | linking_encoding=None, document_encoding=None): 24 | """ 25 | Determine the character encoding from the passed metadata and the 26 | ``@charset`` rule in the stylesheet (if any); and decode accordingly. 27 | If no encoding information is available or decoding fails, 28 | decoding defaults to UTF-8 and then fall back on ISO-8859-1. 29 | 30 | :param css_bytes: 31 | a CSS stylesheet as a byte string 32 | :param protocol_encoding: 33 | The "charset" parameter of a "Content-Type" HTTP header (if any), 34 | or similar metadata for other protocols. 35 | :param linking_encoding: 36 | ```` or other metadata from the linking mechanism 37 | (if any) 38 | :param document_encoding: 39 | Encoding of the referring style sheet or document (if any) 40 | :return: 41 | A tuple of an Unicode string, with any BOM removed, and the 42 | encoding that was used. 43 | 44 | """ 45 | if protocol_encoding: 46 | css_unicode = try_encoding(css_bytes, protocol_encoding) 47 | if css_unicode is not None: 48 | return css_unicode, protocol_encoding 49 | for encoding, pattern in ENCODING_MAGIC_NUMBERS: 50 | match = pattern(css_bytes) 51 | if match: 52 | has_at_charset = isinstance(encoding, tuple) 53 | if has_at_charset: 54 | extract, endianness = encoding 55 | encoding = extract(match.group(1)) 56 | # Get an ASCII-only unicode value. 57 | # This is the only thing that works on both Python 2 and 3 58 | # for bytes.decode() 59 | # Non-ASCII encoding names are invalid anyway, 60 | # but make sure they stay invalid. 61 | encoding = encoding.decode('ascii', 'replace') 62 | encoding = encoding.replace('\ufffd', '?') 63 | if encoding.replace('-', '').replace('_', '').lower() in [ 64 | 'utf16', 'utf32']: 65 | encoding += endianness 66 | encoding = encoding.encode('ascii', 'replace').decode('ascii') 67 | css_unicode = try_encoding(css_bytes, encoding) 68 | if css_unicode and not (has_at_charset and not 69 | css_unicode.startswith('@charset "')): 70 | return css_unicode, encoding 71 | break 72 | for encoding in [linking_encoding, document_encoding]: 73 | if encoding: 74 | css_unicode = try_encoding(css_bytes, encoding) 75 | if css_unicode is not None: 76 | return css_unicode, encoding 77 | css_unicode = try_encoding(css_bytes, 'UTF-8') 78 | if css_unicode is not None: 79 | return css_unicode, 'UTF-8' 80 | return try_encoding(css_bytes, 'ISO-8859-1', fallback=False), 'ISO-8859-1' 81 | 82 | 83 | def try_encoding(css_bytes, encoding, fallback=True): 84 | if fallback: 85 | try: 86 | css_unicode = css_bytes.decode(encoding) 87 | # LookupError means unknown encoding 88 | except (UnicodeDecodeError, LookupError): 89 | return None 90 | else: 91 | css_unicode = css_bytes.decode(encoding) 92 | if css_unicode and css_unicode[0] == '\ufeff': 93 | # Remove any Byte Order Mark 94 | css_unicode = css_unicode[1:] 95 | return css_unicode 96 | 97 | 98 | def hex2re(hex_data): 99 | return re.escape(unhexlify(hex_data.replace(' ', '').encode('ascii'))) 100 | 101 | 102 | class Slicer(object): 103 | """Slice()[start:stop:end] == slice(start, stop, end)""" 104 | def __getitem__(self, slice_): 105 | return operator.itemgetter(slice_) 106 | 107 | Slice = Slicer() 108 | 109 | 110 | # List of (bom_size, encoding, pattern) 111 | # bom_size is in bytes and can be zero 112 | # encoding is a string or (slice_, endianness) for "as specified" 113 | # slice_ is a slice object.How to extract the specified 114 | 115 | ENCODING_MAGIC_NUMBERS = [ 116 | ((Slice[:], ''), re.compile( 117 | hex2re('EF BB BF 40 63 68 61 72 73 65 74 20 22') + 118 | b'([^\x22]*?)' + 119 | hex2re('22 3B')).match), 120 | 121 | ('UTF-8', re.compile( 122 | hex2re('EF BB BF')).match), 123 | 124 | ((Slice[:], ''), re.compile( 125 | hex2re('40 63 68 61 72 73 65 74 20 22') + 126 | b'([^\x22]*?)' + 127 | hex2re('22 3B')).match), 128 | 129 | ((Slice[1::2], '-BE'), re.compile( 130 | hex2re('FE FF 00 40 00 63 00 68 00 61 00 72 00 73 00 65 00' 131 | '74 00 20 00 22') + 132 | b'((\x00[^\x22])*?)' + 133 | hex2re('00 22 00 3B')).match), 134 | 135 | ((Slice[1::2], '-BE'), re.compile( 136 | hex2re('00 40 00 63 00 68 00 61 00 72 00 73 00 65 00 74 00' 137 | '20 00 22') + 138 | b'((\x00[^\x22])*?)' + 139 | hex2re('00 22 00 3B')).match), 140 | 141 | ((Slice[::2], '-LE'), re.compile( 142 | hex2re('FF FE 40 00 63 00 68 00 61 00 72 00 73 00 65 00 74' 143 | '00 20 00 22 00') + 144 | b'(([^\x22]\x00)*?)' + 145 | hex2re('22 00 3B 00')).match), 146 | 147 | ((Slice[::2], '-LE'), re.compile( 148 | hex2re('40 00 63 00 68 00 61 00 72 00 73 00 65 00 74 00 20' 149 | '00 22 00') + 150 | b'(([^\x22]\x00)*?)' + 151 | hex2re('22 00 3B 00')).match), 152 | 153 | ((Slice[3::4], '-BE'), re.compile( 154 | hex2re('00 00 FE FF 00 00 00 40 00 00 00 63 00 00 00 68 00' 155 | '00 00 61 00 00 00 72 00 00 00 73 00 00 00 65 00 00' 156 | '00 74 00 00 00 20 00 00 00 22') + 157 | b'((\x00\x00\x00[^\x22])*?)' + 158 | hex2re('00 00 00 22 00 00 00 3B')).match), 159 | 160 | ((Slice[3::4], '-BE'), re.compile( 161 | hex2re('00 00 00 40 00 00 00 63 00 00 00 68 00 00 00 61 00' 162 | '00 00 72 00 00 00 73 00 00 00 65 00 00 00 74 00 00' 163 | '00 20 00 00 00 22') + 164 | b'((\x00\x00\x00[^\x22])*?)' + 165 | hex2re('00 00 00 22 00 00 00 3B')).match), 166 | 167 | 168 | # Python does not support 2143 or 3412 endianness, AFAIK. 169 | # I guess we could fix it up ourselves but meh. Patches welcome. 170 | 171 | # ((Slice[2::4], '-2143'), re.compile( 172 | # hex2re('00 00 FF FE 00 00 40 00 00 00 63 00 00 00 68 00 00' 173 | # '00 61 00 00 00 72 00 00 00 73 00 00 00 65 00 00 00' 174 | # '74 00 00 00 20 00 00 00 22 00') + 175 | # b'((\x00\x00[^\x22]\x00)*?)' + 176 | # hex2re('00 00 22 00 00 00 3B 00')).match), 177 | 178 | # ((Slice[2::4], '-2143'), re.compile( 179 | # hex2re('00 00 40 00 00 00 63 00 00 00 68 00 00 00 61 00 00' 180 | # '00 72 00 00 00 73 00 00 00 65 00 00 00 74 00 00 00' 181 | # '20 00 00 00 22 00') + 182 | # b'((\x00\x00[^\x22]\x00)*?)' + 183 | # hex2re('00 00 22 00 00 00 3B 00')).match), 184 | 185 | # ((Slice[1::4], '-3412'), re.compile( 186 | # hex2re('FE FF 00 00 00 40 00 00 00 63 00 00 00 68 00 00 00' 187 | # '61 00 00 00 72 00 00 00 73 00 00 00 65 00 00 00 74' 188 | # '00 00 00 20 00 00 00 22 00 00') + 189 | # b'((\x00[^\x22]\x00\x00)*?)' + 190 | # hex2re('00 22 00 00 00 3B 00 00')).match), 191 | 192 | # ((Slice[1::4], '-3412'), re.compile( 193 | # hex2re('00 40 00 00 00 63 00 00 00 68 00 00 00 61 00 00 00' 194 | # '72 00 00 00 73 00 00 00 65 00 00 00 74 00 00 00 20' 195 | # '00 00 00 22 00 00') + 196 | # b'((\x00[^\x22]\x00\x00)*?)' + 197 | # hex2re('00 22 00 00 00 3B 00 00')).match), 198 | 199 | ((Slice[::4], '-LE'), re.compile( 200 | hex2re('FF FE 00 00 40 00 00 00 63 00 00 00 68 00 00 00 61' 201 | '00 00 00 72 00 00 00 73 00 00 00 65 00 00 00 74 00' 202 | '00 00 20 00 00 00 22 00 00 00') + 203 | b'(([^\x22]\x00\x00\x00)*?)' + 204 | hex2re('22 00 00 00 3B 00 00 00')).match), 205 | 206 | ((Slice[::4], '-LE'), re.compile( 207 | hex2re('40 00 00 00 63 00 00 00 68 00 00 00 61 00 00 00 72' 208 | '00 00 00 73 00 00 00 65 00 00 00 74 00 00 00 20 00' 209 | '00 00 22 00 00 00') + 210 | b'(([^\x22]\x00\x00\x00)*?)' + 211 | hex2re('22 00 00 00 3B 00 00 00')).match), 212 | 213 | ('UTF-32-BE', re.compile( 214 | hex2re('00 00 FE FF')).match), 215 | 216 | ('UTF-32-LE', re.compile( 217 | hex2re('FF FE 00 00')).match), 218 | 219 | # ('UTF-32-2143', re.compile( 220 | # hex2re('00 00 FF FE')).match), 221 | 222 | # ('UTF-32-3412', re.compile( 223 | # hex2re('FE FF 00 00')).match), 224 | 225 | ('UTF-16-BE', re.compile( 226 | hex2re('FE FF')).match), 227 | 228 | ('UTF-16-LE', re.compile( 229 | hex2re('FF FE')).match), 230 | 231 | 232 | # Some of there are supported by Python, but I didn’t bother. 233 | # You know the story with patches ... 234 | 235 | # # as specified, transcoded from EBCDIC to ASCII 236 | # ('as_specified-EBCDIC', re.compile( 237 | # hex2re('7C 83 88 81 99 A2 85 A3 40 7F') 238 | # + b'([^\x7F]*?)' 239 | # + hex2re('7F 5E')).match), 240 | 241 | # # as specified, transcoded from IBM1026 to ASCII 242 | # ('as_specified-IBM1026', re.compile( 243 | # hex2re('AE 83 88 81 99 A2 85 A3 40 FC') 244 | # + b'([^\xFC]*?)' 245 | # + hex2re('FC 5E')).match), 246 | 247 | # # as specified, transcoded from GSM 03.38 to ASCII 248 | # ('as_specified-GSM_03.38', re.compile( 249 | # hex2re('00 63 68 61 72 73 65 74 20 22') 250 | # + b'([^\x22]*?)' 251 | # + hex2re('22 3B')).match), 252 | ] 253 | -------------------------------------------------------------------------------- /python3.7libs/tinycss/fonts3.py: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | """ 3 | tinycss.colors3 4 | --------------- 5 | 6 | Parser for CSS 3 Fonts syntax: 7 | https://www.w3.org/TR/css-fonts-3/ 8 | 9 | Adds support for font-face and font-feature-values rules. 10 | 11 | :copyright: (c) 2016 by Kozea. 12 | :license: BSD, see LICENSE for more details. 13 | """ 14 | 15 | from __future__ import division, unicode_literals 16 | 17 | from .css21 import CSS21Parser, ParseError 18 | 19 | 20 | class FontFaceRule(object): 21 | """A parsed at-rule for font faces. 22 | 23 | .. attribute:: at_keyword 24 | 25 | Always ``'@font-face'``. 26 | 27 | .. attribute:: declarations 28 | 29 | A list of :class:`~.css21.Declaration` objects. 30 | 31 | .. attribute:: line 32 | 33 | Source line where this was read. 34 | 35 | .. attribute:: column 36 | 37 | Source column where this was read. 38 | 39 | """ 40 | 41 | def __init__(self, at_keyword, declarations, line, column): 42 | assert at_keyword == '@font-face' 43 | self.at_keyword = at_keyword 44 | self.declarations = declarations 45 | self.line = line 46 | self.column = column 47 | 48 | 49 | class FontFeatureValuesRule(object): 50 | """A parsed at-rule for font feature values. 51 | 52 | .. attribute:: at_keyword 53 | 54 | Always ``'@font-feature-values'``. 55 | 56 | .. attribute:: line 57 | 58 | Source line where this was read. 59 | 60 | .. attribute:: column 61 | 62 | Source column where this was read. 63 | 64 | .. attribute:: at_rules 65 | 66 | The list of parsed at-rules inside the @font-feature-values block, in 67 | source order. 68 | 69 | .. attribute:: family_names 70 | 71 | A list of strings representing font families. 72 | 73 | """ 74 | 75 | def __init__(self, at_keyword, at_rules, family_names, line, column): 76 | assert at_keyword == '@font-feature-values' 77 | self.at_keyword = at_keyword 78 | self.family_names = family_names 79 | self.at_rules = at_rules 80 | self.line = line 81 | self.column = column 82 | 83 | 84 | class FontFeatureRule(object): 85 | """A parsed at-rule for font features. 86 | 87 | .. attribute:: at_keyword 88 | 89 | One of the 16 following strings: 90 | 91 | * ``@stylistic`` 92 | * ``@styleset`` 93 | * ``@character-variant`` 94 | * ``@swash`` 95 | * ``@ornaments`` 96 | * ``@annotation`` 97 | 98 | .. attribute:: declarations 99 | 100 | A list of :class:`~.css21.Declaration` objects. 101 | 102 | .. attribute:: line 103 | 104 | Source line where this was read. 105 | 106 | .. attribute:: column 107 | 108 | Source column where this was read. 109 | 110 | """ 111 | 112 | def __init__(self, at_keyword, declarations, line, column): 113 | self.at_keyword = at_keyword 114 | self.declarations = declarations 115 | self.line = line 116 | self.column = column 117 | 118 | 119 | class CSSFonts3Parser(CSS21Parser): 120 | """Extend :class:`~.css21.CSS21Parser` for `CSS 3 Fonts`_ syntax. 121 | 122 | .. _CSS 3 Fonts: https://www.w3.org/TR/css-fonts-3/ 123 | 124 | """ 125 | 126 | FONT_FEATURE_VALUES_AT_KEYWORDS = [ 127 | '@stylistic', 128 | '@styleset', 129 | '@character-variant', 130 | '@swash', 131 | '@ornaments', 132 | '@annotation', 133 | ] 134 | 135 | def parse_at_rule(self, rule, previous_rules, errors, context): 136 | if rule.at_keyword == '@font-face': 137 | if rule.head: 138 | raise ParseError( 139 | rule.head[0], 140 | 'unexpected {0} token in {1} rule header'.format( 141 | rule.head[0].type, rule.at_keyword)) 142 | declarations, body_errors = self.parse_declaration_list(rule.body) 143 | errors.extend(body_errors) 144 | return FontFaceRule( 145 | rule.at_keyword, declarations, rule.line, rule.column) 146 | elif rule.at_keyword == '@font-feature-values': 147 | family_names = tuple( 148 | self.parse_font_feature_values_family_names(rule.head)) 149 | at_rules, body_errors = ( 150 | self.parse_rules(rule.body or [], '@font-feature-values')) 151 | errors.extend(body_errors) 152 | return FontFeatureValuesRule( 153 | rule.at_keyword, at_rules, family_names, 154 | rule.line, rule.column) 155 | elif rule.at_keyword in self.FONT_FEATURE_VALUES_AT_KEYWORDS: 156 | if context != '@font-feature-values': 157 | raise ParseError( 158 | rule, '{0} rule not allowed in {1}'.format( 159 | rule.at_keyword, context)) 160 | declarations, body_errors = self.parse_declaration_list(rule.body) 161 | errors.extend(body_errors) 162 | return FontFeatureRule( 163 | rule.at_keyword, declarations, rule.line, rule.column) 164 | return super(CSSFonts3Parser, self).parse_at_rule( 165 | rule, previous_rules, errors, context) 166 | 167 | def parse_font_feature_values_family_names(self, tokens): 168 | """Parse an @font-feature-values selector. 169 | 170 | :param tokens: 171 | An iterable of token, typically from the ``head`` attribute of 172 | an unparsed :class:`AtRule`. 173 | :returns: 174 | A generator of strings representing font families. 175 | :raises: 176 | :class:`~.parsing.ParseError` on invalid selectors 177 | 178 | """ 179 | family = '' 180 | current_string = False 181 | for token in tokens: 182 | if token.type == 'DELIM' and token.value == ',' and family: 183 | yield family 184 | family = '' 185 | current_string = False 186 | elif token.type == 'STRING' and not family and ( 187 | current_string is False): 188 | family = token.value 189 | current_string = True 190 | elif token.type == 'IDENT' and not current_string: 191 | if family: 192 | family += ' ' 193 | family += token.value 194 | elif token.type != 'S': 195 | family = '' 196 | break 197 | if family: 198 | yield family 199 | else: 200 | raise ParseError(token, 'invalid @font-feature-values selector') 201 | -------------------------------------------------------------------------------- /python3.7libs/tinycss/page3.py: -------------------------------------------------------------------------------- 1 | # coding: utf-8 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 division, unicode_literals 16 | 17 | from .css21 import CSS21Parser, ParseError 18 | 19 | 20 | class MarginRule(object): 21 | """A parsed at-rule for margin box. 22 | 23 | .. attribute:: at_keyword 24 | 25 | One of the 16 following strings: 26 | 27 | * ``@top-left-corner`` 28 | * ``@top-left`` 29 | * ``@top-center`` 30 | * ``@top-right`` 31 | * ``@top-right-corner`` 32 | * ``@bottom-left-corner`` 33 | * ``@bottom-left`` 34 | * ``@bottom-center`` 35 | * ``@bottom-right`` 36 | * ``@bottom-right-corner`` 37 | * ``@left-top`` 38 | * ``@left-middle`` 39 | * ``@left-bottom`` 40 | * ``@right-top`` 41 | * ``@right-middle`` 42 | * ``@right-bottom`` 43 | 44 | .. attribute:: declarations 45 | 46 | A list of :class:`~.css21.Declaration` objects. 47 | 48 | .. attribute:: line 49 | 50 | Source line where this was read. 51 | 52 | .. attribute:: column 53 | 54 | Source column where this was read. 55 | 56 | """ 57 | 58 | def __init__(self, at_keyword, declarations, line, column): 59 | self.at_keyword = at_keyword 60 | self.declarations = declarations 61 | self.line = line 62 | self.column = column 63 | 64 | 65 | class CSSPage3Parser(CSS21Parser): 66 | """Extend :class:`~.css21.CSS21Parser` for `CSS 3 Paged Media`_ syntax. 67 | 68 | .. _CSS 3 Paged Media: http://dev.w3.org/csswg/css3-page/ 69 | 70 | Compared to CSS 2.1, the ``at_rules`` and ``selector`` attributes of 71 | :class:`~.css21.PageRule` objects are modified: 72 | 73 | * ``at_rules`` is not always empty, it is a list of :class:`MarginRule` 74 | objects. 75 | 76 | * ``selector``, instead of a single string, is a tuple of the page name 77 | and the pseudo class. Each of these may be a ``None`` or a string. 78 | 79 | +--------------------------+------------------------+ 80 | | CSS | Parsed selectors | 81 | +==========================+========================+ 82 | | .. code-block:: css | .. code-block:: python | 83 | | | | 84 | | @page {} | (None, None) | 85 | | @page :first {} | (None, 'first') | 86 | | @page chapter {} | ('chapter', None) | 87 | | @page table:right {} | ('table', 'right') | 88 | +--------------------------+------------------------+ 89 | 90 | """ 91 | 92 | PAGE_MARGIN_AT_KEYWORDS = [ 93 | '@top-left-corner', 94 | '@top-left', 95 | '@top-center', 96 | '@top-right', 97 | '@top-right-corner', 98 | '@bottom-left-corner', 99 | '@bottom-left', 100 | '@bottom-center', 101 | '@bottom-right', 102 | '@bottom-right-corner', 103 | '@left-top', 104 | '@left-middle', 105 | '@left-bottom', 106 | '@right-top', 107 | '@right-middle', 108 | '@right-bottom', 109 | ] 110 | 111 | def parse_at_rule(self, rule, previous_rules, errors, context): 112 | if rule.at_keyword in self.PAGE_MARGIN_AT_KEYWORDS: 113 | if context != '@page': 114 | raise ParseError( 115 | rule, '{0} rule not allowed in {1}'.format( 116 | rule.at_keyword, context)) 117 | if rule.head: 118 | raise ParseError( 119 | rule.head[0], 120 | 'unexpected {0} token in {1} rule header'.format( 121 | rule.head[0].type, rule.at_keyword)) 122 | declarations, body_errors = self.parse_declaration_list(rule.body) 123 | errors.extend(body_errors) 124 | return MarginRule( 125 | rule.at_keyword, declarations, rule.line, rule.column) 126 | return super(CSSPage3Parser, self).parse_at_rule( 127 | rule, previous_rules, errors, context) 128 | 129 | def parse_page_selector(self, head): 130 | """Parse an @page selector. 131 | 132 | :param head: 133 | The ``head`` attribute of an unparsed :class:`AtRule`. 134 | :returns: 135 | A page selector. For CSS 2.1, this is 'first', 'left', 'right' 136 | or None. 'blank' is added by GCPM. 137 | :raises: 138 | :class`~parsing.ParseError` on invalid selectors 139 | 140 | """ 141 | if not head: 142 | return (None, None), (0, 0, 0) 143 | if head[0].type == 'IDENT': 144 | name = head.pop(0).value 145 | while head and head[0].type == 'S': 146 | head.pop(0) 147 | if not head: 148 | return (name, None), (1, 0, 0) 149 | name_specificity = (1,) 150 | else: 151 | name = None 152 | name_specificity = (0,) 153 | if (len(head) == 2 and head[0].type == ':' and 154 | head[1].type == 'IDENT'): 155 | pseudo_class = head[1].value 156 | specificity = { 157 | 'first': (1, 0), 'blank': (1, 0), 158 | 'left': (0, 1), 'right': (0, 1), 159 | }.get(pseudo_class) 160 | if specificity: 161 | return (name, pseudo_class), (name_specificity + specificity) 162 | raise ParseError(head[0], 'invalid @page selector') 163 | -------------------------------------------------------------------------------- /python3.7libs/tinycss/parsing.py: -------------------------------------------------------------------------------- 1 | # coding: utf-8 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 | 99 | def validate_block(tokens, context): 100 | """ 101 | :raises: 102 | :class:`ParseError` if there is any invalid token for the 'block' 103 | production of the core grammar. 104 | :param tokens: an iterable of tokens 105 | :param context: a string for the 'unexpected in ...' message 106 | 107 | """ 108 | for token in tokens: 109 | type_ = token.type 110 | if type_ == '{': 111 | validate_block(token.content, context) 112 | elif type_ not in (';', 'ATKEYWORD'): 113 | validate_any(token, context) 114 | 115 | 116 | def validate_any(token, context): 117 | """ 118 | :raises: 119 | :class:`ParseError` if this is an invalid token for the 120 | 'any' production of the core grammar. 121 | :param token: a single token 122 | :param context: a string for the 'unexpected in ...' message 123 | 124 | """ 125 | type_ = token.type 126 | if type_ in ('FUNCTION', '(', '['): 127 | for token in token.content: 128 | validate_any(token, type_) 129 | elif type_ not in ('S', 'IDENT', 'DIMENSION', 'PERCENTAGE', 'NUMBER', 130 | 'INTEGER', 'URI', 'DELIM', 'STRING', 'HASH', ':', 131 | 'UNICODE-RANGE'): 132 | if type_ in ('}', ')', ']'): 133 | adjective = 'unmatched' 134 | else: 135 | adjective = 'unexpected' 136 | raise ParseError( 137 | token, '{0} {1} token in {2}'.format(adjective, type_, context)) 138 | 139 | 140 | class ParseError(ValueError): 141 | """Details about a CSS syntax error. Usually indicates that something 142 | (a rule or a declaration) was ignored and will not appear as a parsed 143 | object. 144 | 145 | This exception is typically logged in a list rather than being propagated 146 | to the user API. 147 | 148 | .. attribute:: line 149 | 150 | Source line where the error occured. 151 | 152 | .. attribute:: column 153 | 154 | Column in the source line where the error occured. 155 | 156 | .. attribute:: reason 157 | 158 | What happend (a string). 159 | 160 | """ 161 | def __init__(self, subject, reason): 162 | self.line = subject.line 163 | self.column = subject.column 164 | self.reason = reason 165 | super(ParseError, self).__init__( 166 | 'Parse error at {0.line}:{0.column}, {0.reason}'.format(self)) 167 | -------------------------------------------------------------------------------- /python3.7libs/tinycss/token_data.py: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | """ 3 | tinycss.token_data 4 | ------------------ 5 | 6 | Shared data for both implementations (Cython and Python) of the tokenizer. 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 | import functools 15 | import operator 16 | import re 17 | import string 18 | import sys 19 | 20 | # * Raw strings with the r'' notation are used so that \ do not need 21 | # to be escaped. 22 | # * Names and regexps are separated by a tabulation. 23 | # * Macros are re-ordered so that only previous definitions are needed. 24 | # * {} are used for macro substitution with ``string.Formatter``, 25 | # so other uses of { or } have been doubled. 26 | # * The syntax is otherwise compatible with re.compile. 27 | # * Some parentheses were added to add capturing groups. 28 | # (in unicode, DIMENSION and URI) 29 | 30 | # *** Willful violation: *** 31 | # Numbers can take a + or - sign, but the sign is a separate DELIM token. 32 | # Since comments are allowed anywhere between tokens, this makes 33 | # the following this is valid. It means 10 negative pixels: 34 | # margin-top: -/**/10px 35 | 36 | # This makes parsing numbers a pain, so instead we’ll do the same is Firefox 37 | # and make the sign part as of the 'num' macro. The above CSS will be invalid. 38 | # See discussion: 39 | # http://lists.w3.org/Archives/Public/www-style/2011Oct/0028.html 40 | MACROS = r''' 41 | nl \n|\r\n|\r|\f 42 | w [ \t\r\n\f]* 43 | nonascii [^\0-\237] 44 | unicode \\([0-9a-f]{{1,6}})(\r\n|[ \n\r\t\f])? 45 | simple_escape [^\n\r\f0-9a-f] 46 | escape {unicode}|\\{simple_escape} 47 | nmstart [_a-z]|{nonascii}|{escape} 48 | nmchar [_a-z0-9-]|{nonascii}|{escape} 49 | name {nmchar}+ 50 | ident [-]?{nmstart}{nmchar}* 51 | num [-+]?(?:[0-9]*\.[0-9]+|[0-9]+) 52 | string1 \"([^\n\r\f\\"]|\\{nl}|{escape})*\" 53 | string2 \'([^\n\r\f\\']|\\{nl}|{escape})*\' 54 | string {string1}|{string2} 55 | badstring1 \"([^\n\r\f\\"]|\\{nl}|{escape})*\\? 56 | badstring2 \'([^\n\r\f\\']|\\{nl}|{escape})*\\? 57 | badstring {badstring1}|{badstring2} 58 | badcomment1 \/\*[^*]*\*+([^/*][^*]*\*+)* 59 | badcomment2 \/\*[^*]*(\*+[^/*][^*]*)* 60 | badcomment {badcomment1}|{badcomment2} 61 | baduri1 url\({w}([!#$%&*-~]|{nonascii}|{escape})*{w} 62 | baduri2 url\({w}{string}{w} 63 | baduri3 url\({w}{badstring} 64 | baduri {baduri1}|{baduri2}|{baduri3} 65 | '''.replace(r'\0', '\0').replace(r'\237', '\237') 66 | 67 | # Removed these tokens. Instead, they’re tokenized as two DELIM each. 68 | # INCLUDES ~= 69 | # DASHMATCH |= 70 | # They are only used in selectors but selectors3 also have ^=, *= and $=. 71 | # We don’t actually parse selectors anyway 72 | 73 | # Re-ordered so that the longest match is always the first. 74 | # For example, "url('foo')" matches URI, BAD_URI, FUNCTION and IDENT, 75 | # but URI would always be a longer match than the others. 76 | TOKENS = r''' 77 | S [ \t\r\n\f]+ 78 | 79 | URI url\({w}({string}|([!#$%&*-\[\]-~]|{nonascii}|{escape})*){w}\) 80 | BAD_URI {baduri} 81 | FUNCTION {ident}\( 82 | UNICODE-RANGE u\+[0-9a-f?]{{1,6}}(-[0-9a-f]{{1,6}})? 83 | IDENT {ident} 84 | 85 | ATKEYWORD @{ident} 86 | HASH #{name} 87 | 88 | DIMENSION ({num})({ident}) 89 | PERCENTAGE {num}% 90 | NUMBER {num} 91 | 92 | STRING {string} 93 | BAD_STRING {badstring} 94 | 95 | COMMENT \/\*[^*]*\*+([^/*][^*]*\*+)*\/ 96 | BAD_COMMENT {badcomment} 97 | 98 | : : 99 | ; ; 100 | { \{{ 101 | } \}} 102 | ( \( 103 | ) \) 104 | [ \[ 105 | ] \] 106 | CDO 108 | ''' 109 | 110 | 111 | # Strings with {macro} expanded 112 | COMPILED_MACROS = {} 113 | 114 | 115 | COMPILED_TOKEN_REGEXPS = [] # [(name, regexp.match)] ordered 116 | COMPILED_TOKEN_INDEXES = {} # {name: i} helper for the C speedups 117 | 118 | 119 | # Indexed by codepoint value of the first character of a token. 120 | # Codepoints >= 160 (aka nonascii) all use the index 160. 121 | # values are (i, name, regexp.match) 122 | TOKEN_DISPATCH = [] 123 | 124 | 125 | try: 126 | unichr 127 | except NameError: 128 | # Python 3 129 | unichr = chr 130 | unicode = str 131 | 132 | 133 | def _init(): 134 | """Import-time initialization.""" 135 | COMPILED_MACROS.clear() 136 | for line in MACROS.splitlines(): 137 | if line.strip(): 138 | name, value = line.split('\t') 139 | COMPILED_MACROS[name.strip()] = '(?:%s)' \ 140 | % value.format(**COMPILED_MACROS) 141 | 142 | COMPILED_TOKEN_REGEXPS[:] = ( 143 | ( 144 | name.strip(), 145 | re.compile( 146 | value.format(**COMPILED_MACROS), 147 | # Case-insensitive when matching eg. uRL(foo) 148 | # but preserve the case in extracted groups 149 | re.I 150 | ).match 151 | ) 152 | for line in TOKENS.splitlines() 153 | if line.strip() 154 | for name, value in [line.split('\t')] 155 | ) 156 | 157 | COMPILED_TOKEN_INDEXES.clear() 158 | for i, (name, regexp) in enumerate(COMPILED_TOKEN_REGEXPS): 159 | COMPILED_TOKEN_INDEXES[name] = i 160 | 161 | dispatch = [[] for i in range(161)] 162 | for chars, names in [ 163 | (' \t\r\n\f', ['S']), 164 | ('uU', ['URI', 'BAD_URI', 'UNICODE-RANGE']), 165 | # \ is an escape outside of another token 166 | (string.ascii_letters + '\\_-' + unichr(160), ['FUNCTION', 'IDENT']), 167 | (string.digits + '.+-', ['DIMENSION', 'PERCENTAGE', 'NUMBER']), 168 | ('@', ['ATKEYWORD']), 169 | ('#', ['HASH']), 170 | ('\'"', ['STRING', 'BAD_STRING']), 171 | ('/', ['COMMENT', 'BAD_COMMENT']), 172 | ('<', ['CDO']), 173 | ('-', ['CDC']), 174 | ]: 175 | for char in chars: 176 | dispatch[ord(char)].extend(names) 177 | for char in ':;{}()[]': 178 | dispatch[ord(char)] = [char] 179 | 180 | TOKEN_DISPATCH[:] = ( 181 | [ 182 | (index,) + COMPILED_TOKEN_REGEXPS[index] 183 | for name in names 184 | for index in [COMPILED_TOKEN_INDEXES[name]] 185 | ] 186 | for names in dispatch 187 | ) 188 | 189 | _init() 190 | 191 | 192 | def _unicode_replace(match, int=int, unichr=unichr, maxunicode=sys.maxunicode): 193 | codepoint = int(match.group(1), 16) 194 | if codepoint <= maxunicode: 195 | return unichr(codepoint) 196 | else: 197 | return '\N{REPLACEMENT CHARACTER}' # U+FFFD 198 | 199 | UNICODE_UNESCAPE = functools.partial( 200 | re.compile(COMPILED_MACROS['unicode'], re.I).sub, 201 | _unicode_replace) 202 | 203 | NEWLINE_UNESCAPE = functools.partial( 204 | re.compile(r'()\\' + COMPILED_MACROS['nl']).sub, 205 | '') 206 | 207 | SIMPLE_UNESCAPE = functools.partial( 208 | re.compile(r'\\(%s)' % COMPILED_MACROS['simple_escape'], re.I).sub, 209 | # Same as r'\1', but faster on CPython 210 | operator.methodcaller('group', 1)) 211 | 212 | FIND_NEWLINES = re.compile(COMPILED_MACROS['nl']).finditer 213 | 214 | 215 | class Token(object): 216 | """A single atomic token. 217 | 218 | .. attribute:: is_container 219 | 220 | Always ``False``. 221 | Helps to tell :class:`Token` apart from :class:`ContainerToken`. 222 | 223 | .. attribute:: type 224 | 225 | The type of token as a string: 226 | 227 | ``S`` 228 | A sequence of white space 229 | 230 | ``IDENT`` 231 | An identifier: a name that does not start with a digit. 232 | A name is a sequence of letters, digits, ``_``, ``-``, escaped 233 | characters and non-ASCII characters. Eg: ``margin-left`` 234 | 235 | ``HASH`` 236 | ``#`` followed immediately by a name. Eg: ``#ff8800`` 237 | 238 | ``ATKEYWORD`` 239 | ``@`` followed immediately by an identifier. Eg: ``@page`` 240 | 241 | ``URI`` 242 | Eg: ``url(foo)`` The content may or may not be quoted. 243 | 244 | ``UNICODE-RANGE`` 245 | ``U+`` followed by one or two hexadecimal 246 | Unicode codepoints. Eg: ``U+20-00FF`` 247 | 248 | ``INTEGER`` 249 | An integer with an optional ``+`` or ``-`` sign 250 | 251 | ``NUMBER`` 252 | A non-integer number with an optional ``+`` or ``-`` sign 253 | 254 | ``DIMENSION`` 255 | An integer or number followed immediately by an 256 | identifier (the unit). Eg: ``12px`` 257 | 258 | ``PERCENTAGE`` 259 | An integer or number followed immediately by ``%`` 260 | 261 | ``STRING`` 262 | A string, quoted with ``"`` or ``'`` 263 | 264 | ``:`` or ``;`` 265 | That character. 266 | 267 | ``DELIM`` 268 | A single character not matched in another token. Eg: ``,`` 269 | 270 | See the source of the :mod:`.token_data` module for the precise 271 | regular expressions that match various tokens. 272 | 273 | Note that other token types exist in the early tokenization steps, 274 | but these are ignored, are syntax errors, or are later transformed 275 | into :class:`ContainerToken` or :class:`FunctionToken`. 276 | 277 | .. attribute:: value 278 | 279 | The parsed value: 280 | 281 | * INTEGER, NUMBER, PERCENTAGE or DIMENSION tokens: the numeric value 282 | as an int or float. 283 | * STRING tokens: the unescaped string without quotes 284 | * URI tokens: the unescaped URI without quotes or 285 | ``url(`` and ``)`` markers. 286 | * IDENT, ATKEYWORD or HASH tokens: the unescaped token, 287 | with ``@`` or ``#`` markers left as-is 288 | * Other tokens: same as :attr:`as_css` 289 | 290 | *Unescaped* refers to the various escaping methods based on the 291 | backslash ``\`` character in CSS syntax. 292 | 293 | .. attribute:: unit 294 | 295 | * DIMENSION tokens: the normalized (unescaped, lower-case) 296 | unit name as a string. eg. ``'px'`` 297 | * PERCENTAGE tokens: the string ``'%'`` 298 | * Other tokens: ``None`` 299 | 300 | .. attribute:: line 301 | 302 | The line number in the CSS source of the start of this token. 303 | 304 | .. attribute:: column 305 | 306 | The column number (inside a source line) of the start of this token. 307 | 308 | """ 309 | is_container = False 310 | __slots__ = 'type', '_as_css', 'value', 'unit', 'line', 'column' 311 | 312 | def __init__(self, type_, css_value, value, unit, line, column): 313 | self.type = type_ 314 | self._as_css = css_value 315 | self.value = value 316 | self.unit = unit 317 | self.line = line 318 | self.column = column 319 | 320 | def as_css(self): 321 | """ 322 | Return as an Unicode string the CSS representation of the token, 323 | as parsed in the source. 324 | """ 325 | return self._as_css 326 | 327 | def __repr__(self): 328 | return ('' 329 | .format(self, self.unit or '')) 330 | 331 | def __eq__(self, other): 332 | if type(self) != type(other): 333 | raise TypeError( 334 | 'Cannot compare {0} and {1}'.format(type(self), type(other))) 335 | else: 336 | return all( 337 | self.type_ == other.type_, 338 | self._as_css == other._as_css, 339 | self.value == other.value, 340 | self.unit == other.unit, 341 | ) 342 | 343 | 344 | class ContainerToken(object): 345 | """A token that contains other (nested) tokens. 346 | 347 | .. attribute:: is_container 348 | 349 | Always ``True``. 350 | Helps to tell :class:`ContainerToken` apart from :class:`Token`. 351 | 352 | .. attribute:: type 353 | 354 | The type of token as a string. One of ``{``, ``(``, ``[`` or 355 | ``FUNCTION``. For ``FUNCTION``, the object is actually a 356 | :class:`FunctionToken`. 357 | 358 | .. attribute:: unit 359 | 360 | Always ``None``. Included to make :class:`ContainerToken` behave 361 | more like :class:`Token`. 362 | 363 | .. attribute:: content 364 | 365 | A list of :class:`Token` or nested :class:`ContainerToken`, 366 | not including the opening or closing token. 367 | 368 | .. attribute:: line 369 | 370 | The line number in the CSS source of the start of this token. 371 | 372 | .. attribute:: column 373 | 374 | The column number (inside a source line) of the start of this token. 375 | 376 | """ 377 | is_container = True 378 | unit = None 379 | __slots__ = 'type', '_css_start', '_css_end', 'content', 'line', 'column' 380 | 381 | def __init__(self, type_, css_start, css_end, content, line, column): 382 | self.type = type_ 383 | self._css_start = css_start 384 | self._css_end = css_end 385 | self.content = content 386 | self.line = line 387 | self.column = column 388 | 389 | def as_css(self): 390 | """ 391 | Return as an Unicode string the CSS representation of the token, 392 | as parsed in the source. 393 | """ 394 | parts = [self._css_start] 395 | parts.extend(token.as_css() for token in self.content) 396 | parts.append(self._css_end) 397 | return ''.join(parts) 398 | 399 | format_string = '' 400 | 401 | def __repr__(self): 402 | return (self.format_string + ' {0.content}').format(self) 403 | 404 | 405 | class FunctionToken(ContainerToken): 406 | """A specialized :class:`ContainerToken` for a ``FUNCTION`` group. 407 | Has an additional attribute: 408 | 409 | .. attribute:: function_name 410 | 411 | The unescaped name of the function, with the ``(`` marker removed. 412 | 413 | """ 414 | __slots__ = 'function_name', 415 | 416 | def __init__(self, type_, css_start, css_end, function_name, content, 417 | line, column): 418 | super(FunctionToken, self).__init__( 419 | type_, css_start, css_end, content, line, column) 420 | # Remove the ( marker: 421 | self.function_name = function_name[:-1] 422 | 423 | format_string = ('') 425 | 426 | 427 | class TokenList(list): 428 | """ 429 | A mixed list of :class:`~.token_data.Token` and 430 | :class:`~.token_data.ContainerToken` objects. 431 | 432 | This is a subclass of the builtin :class:`~builtins.list` type. 433 | It can be iterated, indexed and sliced as usual, but also has some 434 | additional API: 435 | 436 | """ 437 | @property 438 | def line(self): 439 | """The line number in the CSS source of the first token.""" 440 | return self[0].line 441 | 442 | @property 443 | def column(self): 444 | """The column number (inside a source line) of the first token.""" 445 | return self[0].column 446 | 447 | def as_css(self): 448 | """ 449 | Return as an Unicode string the CSS representation of the tokens, 450 | as parsed in the source. 451 | """ 452 | return ''.join(token.as_css() for token in self) 453 | -------------------------------------------------------------------------------- /python3.7libs/tinycss/tokenizer.py: -------------------------------------------------------------------------------- 1 | # coding: utf-8 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( 21 | css_source, ignore_comments=True, 22 | # Make these local variable to avoid global lookups in the loop 23 | tokens_dispatch=token_data.TOKEN_DISPATCH, 24 | unicode_unescape=token_data.UNICODE_UNESCAPE, 25 | newline_unescape=token_data.NEWLINE_UNESCAPE, 26 | simple_unescape=token_data.SIMPLE_UNESCAPE, 27 | find_newlines=token_data.FIND_NEWLINES, 28 | Token=token_data.Token, 29 | len=len, 30 | int=int, 31 | float=float, 32 | list=list, 33 | _None=None): 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, tokens=tokens, pairs=pairs, eof=eof, 162 | ContainerToken=token_data.ContainerToken, 163 | FunctionToken=token_data.FunctionToken): 164 | for token in tokens: 165 | type_ = token.type 166 | if type_ == stop_at: 167 | return 168 | 169 | end = pairs.get(type_) 170 | if end is None: 171 | yield token # Not a grouping token 172 | else: 173 | assert not isinstance(token, ContainerToken), ( 174 | 'Token looks already grouped: {0}'.format(token)) 175 | content = list(_regroup_inner(end)) 176 | if eof[0]: 177 | end = '' # Implicit end of structure at EOF. 178 | if type_ == 'FUNCTION': 179 | yield FunctionToken(token.type, token.as_css(), end, 180 | token.value, content, 181 | token.line, token.column) 182 | else: 183 | yield ContainerToken(token.type, token.as_css(), end, 184 | content, 185 | token.line, token.column) 186 | else: 187 | eof[0] = True # end of file/stylesheet 188 | return _regroup_inner() 189 | 190 | 191 | def tokenize_grouped(css_source, ignore_comments=True): 192 | """ 193 | :param css_source: 194 | CSS as an unicode string 195 | :param ignore_comments: 196 | if true (the default) comments will not be included in the 197 | return value 198 | :return: 199 | An iterator of :class:`Token` 200 | 201 | """ 202 | return regroup(tokenize_flat(css_source, ignore_comments)) 203 | 204 | 205 | # Optional Cython version of tokenize_flat 206 | # Make both versions available with explicit names for tests. 207 | python_tokenize_flat = tokenize_flat 208 | try: 209 | from . import speedups 210 | except ImportError: 211 | cython_tokenize_flat = None 212 | else: 213 | cython_tokenize_flat = speedups.tokenize_flat 214 | # Default to the Cython version if available 215 | tokenize_flat = cython_tokenize_flat 216 | -------------------------------------------------------------------------------- /python3.7libs/tinycss/version.py: -------------------------------------------------------------------------------- 1 | VERSION = '0.4' 2 | --------------------------------------------------------------------------------