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