304 lines
11 KiB
Python
304 lines
11 KiB
Python
""":mod:`sassutils.builder` --- Build the whole directory
|
|
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
|
|
|
"""
|
|
|
|
import io
|
|
import os
|
|
import os.path
|
|
import re
|
|
import warnings
|
|
|
|
from six import string_types
|
|
|
|
from sass import compile
|
|
from sassutils._compat import collections_abc
|
|
|
|
__all__ = 'SUFFIXES', 'SUFFIX_PATTERN', 'Manifest', 'build_directory'
|
|
|
|
|
|
#: (:class:`frozenset`) The set of supported filename suffixes.
|
|
SUFFIXES = frozenset(('sass', 'scss'))
|
|
|
|
#: (:class:`re.RegexObject`) The regular expression pattern which matches to
|
|
#: filenames of supported :const:`SUFFIXES`.
|
|
SUFFIX_PATTERN = re.compile(
|
|
'[.](' + '|'.join(map(re.escape, sorted(SUFFIXES))) + ')$',
|
|
)
|
|
|
|
|
|
def build_directory(
|
|
sass_path, css_path, output_style='nested',
|
|
_root_sass=None, _root_css=None, strip_extension=False,
|
|
):
|
|
"""Compiles all Sass/SCSS files in ``path`` to CSS.
|
|
|
|
:param sass_path: the path of the directory which contains source files
|
|
to compile
|
|
:type sass_path: :class:`str`, :class:`basestring`
|
|
:param css_path: the path of the directory compiled CSS files will go
|
|
:type css_path: :class:`str`, :class:`basestring`
|
|
:param output_style: an optional coding style of the compiled result.
|
|
choose one of: ``'nested'`` (default), ``'expanded'``,
|
|
``'compact'``, ``'compressed'``
|
|
:type output_style: :class:`str`
|
|
:returns: a dictionary of source filenames to compiled CSS filenames
|
|
:rtype: :class:`collections.abc.Mapping`
|
|
|
|
.. versionadded:: 0.6.0
|
|
The ``output_style`` parameter.
|
|
|
|
"""
|
|
if _root_sass is None or _root_css is None:
|
|
_root_sass = sass_path
|
|
_root_css = css_path
|
|
result = {}
|
|
if not os.path.isdir(css_path):
|
|
os.mkdir(css_path)
|
|
for name in os.listdir(sass_path):
|
|
sass_fullname = os.path.join(sass_path, name)
|
|
if SUFFIX_PATTERN.search(name) and os.path.isfile(sass_fullname):
|
|
if name[0] == '_':
|
|
# Do not compile if it's partial
|
|
continue
|
|
if strip_extension:
|
|
name, _ = os.path.splitext(name)
|
|
css_fullname = os.path.join(css_path, name) + '.css'
|
|
css = compile(
|
|
filename=sass_fullname,
|
|
output_style=output_style,
|
|
include_paths=[_root_sass],
|
|
)
|
|
with io.open(
|
|
css_fullname, 'w', encoding='utf-8', newline='',
|
|
) as css_file:
|
|
css_file.write(css)
|
|
result[os.path.relpath(sass_fullname, _root_sass)] = \
|
|
os.path.relpath(css_fullname, _root_css)
|
|
elif os.path.isdir(sass_fullname):
|
|
css_fullname = os.path.join(css_path, name)
|
|
subresult = build_directory(
|
|
sass_fullname, css_fullname,
|
|
output_style=output_style,
|
|
_root_sass=_root_sass,
|
|
_root_css=_root_css,
|
|
strip_extension=strip_extension,
|
|
)
|
|
result.update(subresult)
|
|
return result
|
|
|
|
|
|
class Manifest(object):
|
|
"""Building manifest of Sass/SCSS.
|
|
|
|
:param sass_path: the path of the directory that contains Sass/SCSS
|
|
source files
|
|
:type sass_path: :class:`str`, :class:`basestring`
|
|
:param css_path: the path of the directory to store compiled CSS
|
|
files
|
|
:type css_path: :class:`str`, :class:`basestring`
|
|
:param strip_extension: whether to remove the original file extension
|
|
:type strip_extension: :class:`bool`
|
|
"""
|
|
|
|
@classmethod
|
|
def normalize_manifests(cls, manifests):
|
|
if manifests is None:
|
|
manifests = {}
|
|
elif isinstance(manifests, collections_abc.Mapping):
|
|
manifests = dict(manifests)
|
|
else:
|
|
raise TypeError(
|
|
'manifests must be a mapping object, not ' +
|
|
repr(manifests),
|
|
)
|
|
for package_name, manifest in manifests.items():
|
|
if not isinstance(package_name, string_types):
|
|
raise TypeError(
|
|
'manifest keys must be a string of package '
|
|
'name, not ' + repr(package_name),
|
|
)
|
|
if isinstance(manifest, Manifest):
|
|
continue
|
|
elif isinstance(manifest, tuple):
|
|
manifest = Manifest(*manifest)
|
|
elif isinstance(manifest, collections_abc.Mapping):
|
|
manifest = Manifest(**manifest)
|
|
elif isinstance(manifest, string_types):
|
|
manifest = Manifest(manifest)
|
|
else:
|
|
raise TypeError(
|
|
'manifest values must be a sassutils.builder.Manifest, '
|
|
'a pair of (sass_path, css_path), or a string of '
|
|
'sass_path, not ' + repr(manifest),
|
|
)
|
|
manifests[package_name] = manifest
|
|
return manifests
|
|
|
|
def __init__(
|
|
self,
|
|
sass_path,
|
|
css_path=None,
|
|
wsgi_path=None,
|
|
strip_extension=None,
|
|
):
|
|
if not isinstance(sass_path, string_types):
|
|
raise TypeError(
|
|
'sass_path must be a string, not ' +
|
|
repr(sass_path),
|
|
)
|
|
if css_path is None:
|
|
css_path = sass_path
|
|
elif not isinstance(css_path, string_types):
|
|
raise TypeError(
|
|
'css_path must be a string, not ' +
|
|
repr(css_path),
|
|
)
|
|
if wsgi_path is None:
|
|
wsgi_path = css_path
|
|
elif not isinstance(wsgi_path, string_types):
|
|
raise TypeError(
|
|
'wsgi_path must be a string, not ' +
|
|
repr(wsgi_path),
|
|
)
|
|
if strip_extension is None:
|
|
warnings.warn(
|
|
'`strip_extension` was not specified, defaulting to `False`.\n'
|
|
'In the future, `strip_extension` will default to `True`.',
|
|
FutureWarning,
|
|
)
|
|
strip_extension = False
|
|
elif not isinstance(strip_extension, bool):
|
|
raise TypeError(
|
|
'strip_extension must be bool not {!r}'.format(
|
|
strip_extension,
|
|
),
|
|
)
|
|
self.sass_path = sass_path
|
|
self.css_path = css_path
|
|
self.wsgi_path = wsgi_path
|
|
self.strip_extension = strip_extension
|
|
|
|
def resolve_filename(self, package_dir, filename):
|
|
"""Gets a proper full relative path of Sass source and
|
|
CSS source that will be generated, according to ``package_dir``
|
|
and ``filename``.
|
|
|
|
:param package_dir: the path of package directory
|
|
:type package_dir: :class:`str`, :class:`basestring`
|
|
:param filename: the filename of Sass/SCSS source to compile
|
|
:type filename: :class:`str`, :class:`basestring`
|
|
:returns: a pair of (sass, css) path
|
|
:rtype: :class:`tuple`
|
|
|
|
"""
|
|
sass_path = os.path.join(package_dir, self.sass_path, filename)
|
|
if self.strip_extension:
|
|
filename, _ = os.path.splitext(filename)
|
|
css_filename = filename + '.css'
|
|
css_path = os.path.join(package_dir, self.css_path, css_filename)
|
|
return sass_path, css_path
|
|
|
|
def unresolve_filename(self, package_dir, filename):
|
|
"""Retrieves the probable source path from the output filename. Pass
|
|
in a .css path to get out a .scss path.
|
|
|
|
:param package_dir: the path of the package directory
|
|
:type package_dir: :class:`str`
|
|
:param filename: the css filename
|
|
:type filename: :class:`str`
|
|
:returns: the scss filename
|
|
:rtype: :class:`str`
|
|
"""
|
|
filename, _ = os.path.splitext(filename)
|
|
if self.strip_extension:
|
|
for ext in ('.scss', '.sass'):
|
|
test_path = os.path.join(
|
|
package_dir, self.sass_path, filename + ext,
|
|
)
|
|
if os.path.exists(test_path):
|
|
return filename + ext
|
|
else: # file not found, let it error with `.scss` extension
|
|
return filename + '.scss'
|
|
else:
|
|
return filename
|
|
|
|
def build(self, package_dir, output_style='nested'):
|
|
"""Builds the Sass/SCSS files in the specified :attr:`sass_path`.
|
|
It finds :attr:`sass_path` and locates :attr:`css_path`
|
|
as relative to the given ``package_dir``.
|
|
|
|
:param package_dir: the path of package directory
|
|
:type package_dir: :class:`str`, :class:`basestring`
|
|
:param output_style: an optional coding style of the compiled result.
|
|
choose one of: ``'nested'`` (default),
|
|
``'expanded'``, ``'compact'``, ``'compressed'``
|
|
:type output_style: :class:`str`
|
|
:returns: the set of compiled CSS filenames
|
|
:rtype: :class:`frozenset`
|
|
|
|
.. versionadded:: 0.6.0
|
|
The ``output_style`` parameter.
|
|
|
|
"""
|
|
sass_path = os.path.join(package_dir, self.sass_path)
|
|
css_path = os.path.join(package_dir, self.css_path)
|
|
css_files = build_directory(
|
|
sass_path, css_path,
|
|
output_style=output_style,
|
|
strip_extension=self.strip_extension,
|
|
).values()
|
|
return frozenset(
|
|
os.path.join(self.css_path, filename)
|
|
for filename in css_files
|
|
)
|
|
|
|
def build_one(self, package_dir, filename, source_map=False):
|
|
"""Builds one Sass/SCSS file.
|
|
|
|
:param package_dir: the path of package directory
|
|
:type package_dir: :class:`str`, :class:`basestring`
|
|
:param filename: the filename of Sass/SCSS source to compile
|
|
:type filename: :class:`str`, :class:`basestring`
|
|
:param source_map: whether to use source maps. if :const:`True`
|
|
it also write a source map to a ``filename``
|
|
followed by :file:`.map` suffix.
|
|
default is :const:`False`
|
|
:type source_map: :class:`bool`
|
|
:returns: the filename of compiled CSS
|
|
:rtype: :class:`str`, :class:`basestring`
|
|
|
|
.. versionadded:: 0.4.0
|
|
Added optional ``source_map`` parameter.
|
|
|
|
"""
|
|
sass_filename, css_filename = self.resolve_filename(
|
|
package_dir, filename,
|
|
)
|
|
root_path = os.path.join(package_dir, self.sass_path)
|
|
css_path = os.path.join(package_dir, self.css_path, css_filename)
|
|
if source_map:
|
|
source_map_path = css_filename + '.map'
|
|
css, source_map = compile(
|
|
filename=sass_filename,
|
|
include_paths=[root_path],
|
|
source_map_filename=source_map_path, # FIXME
|
|
output_filename_hint=css_path,
|
|
)
|
|
else:
|
|
css = compile(filename=sass_filename, include_paths=[root_path])
|
|
source_map_path = None
|
|
source_map = None
|
|
css_folder = os.path.dirname(css_path)
|
|
if not os.path.exists(css_folder):
|
|
os.makedirs(css_folder)
|
|
with io.open(css_path, 'w', encoding='utf-8', newline='') as f:
|
|
f.write(css)
|
|
if source_map:
|
|
# Source maps are JSON, and JSON has to be UTF-8 encoded
|
|
with io.open(
|
|
source_map_path, 'w', encoding='utf-8', newline='',
|
|
) as f:
|
|
f.write(source_map)
|
|
return css_filename
|