Skip to content
This repository was archived by the owner on Oct 24, 2025. It is now read-only.

Commit 7724a49

Browse files
committed
compile() has new params: source_comments, source_map_filename
1 parent 94166b9 commit 7724a49

File tree

4 files changed

+219
-37
lines changed

4 files changed

+219
-37
lines changed

docs/changes.rst

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,13 @@ Version 0.4.0
66

77
To be released.
88

9+
- Expose source maps support:
10+
11+
- :func:`sass.compile()` has a new optional parameter ``source_comments``.
12+
It can be one of :const:`sass.SOURCE_COMMENTS` keys. It also has
13+
a new parameter ``source_map_filename`` which is required only when
14+
``source_comments='map'``.
15+
916
- Fixed Python 3 incompatibility of :program:`sassc` program.
1017

1118

pysass.c

Lines changed: 68 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,43 +1,62 @@
11
#include <unistd.h>
2+
#include <stdlib.h>
3+
#include <string.h>
24
#include <Python.h>
35
#include "sass_interface.h"
46

57
#if PY_MAJOR_VERSION >= 3
68
#define PySass_IF_PY3(three, two) (three)
79
#define PySass_Int_FromLong(v) PyLong_FromLong(v)
10+
#define PySass_Bytes_Check(o) PyBytes_Check(o)
11+
#define PySass_Bytes_GET_SIZE(o) PyBytes_GET_SIZE(o)
12+
#define PySass_Bytes_AS_STRING(o) PyBytes_AS_STRING(o)
813
#else
914
#define PySass_IF_PY3(three, two) (two)
1015
#define PySass_Int_FromLong(v) PyInt_FromLong(v)
16+
#define PySass_Bytes_Check(o) PyString_Check(o)
17+
#define PySass_Bytes_GET_SIZE(o) PyString_GET_SIZE(o)
18+
#define PySass_Bytes_AS_STRING(o) PyString_AS_STRING(o)
1119
#endif
1220

13-
static struct {
21+
struct PySass_Pair {
1422
char *label;
1523
int value;
16-
} PySass_output_style_enum[] = {
24+
};
25+
26+
static struct PySass_Pair PySass_output_style_enum[] = {
1727
{"nested", SASS_STYLE_NESTED},
1828
{"expanded", SASS_STYLE_EXPANDED},
1929
{"compact", SASS_STYLE_COMPACT},
2030
{"compressed", SASS_STYLE_COMPRESSED},
2131
{NULL}
2232
};
2333

34+
static struct PySass_Pair PySass_source_comments_enum[] = {
35+
{"none", SASS_SOURCE_COMMENTS_NONE},
36+
{"line_numbers", SASS_SOURCE_COMMENTS_DEFAULT}, /* alias of "default" */
37+
{"default", SASS_SOURCE_COMMENTS_DEFAULT},
38+
{"map", SASS_SOURCE_COMMENTS_MAP},
39+
{NULL}
40+
};
41+
2442
static PyObject *
2543
PySass_compile_string(PyObject *self, PyObject *args) {
2644
struct sass_context *context;
2745
char *string, *include_paths, *image_path;
28-
int output_style;
46+
int output_style, source_comments;
2947
PyObject *result;
3048

3149
if (!PyArg_ParseTuple(args,
32-
PySass_IF_PY3("yiyy", "siss"),
33-
&string, &output_style,
50+
PySass_IF_PY3("yiiyy", "siiss"),
51+
&string, &output_style, &source_comments,
3452
&include_paths, &image_path)) {
3553
return NULL;
3654
}
3755

3856
context = sass_new_context();
3957
context->source_string = string;
4058
context->options.output_style = output_style;
59+
context->options.source_comments = source_comments;
4160
context->options.include_paths = include_paths;
4261
context->options.image_path = image_path;
4362

@@ -56,28 +75,46 @@ static PyObject *
5675
PySass_compile_filename(PyObject *self, PyObject *args) {
5776
struct sass_file_context *context;
5877
char *filename, *include_paths, *image_path;
59-
int output_style;
60-
PyObject *result;
78+
int output_style, source_comments, error_status;
79+
PyObject *source_map_filename, *result;
6180

6281
if (!PyArg_ParseTuple(args,
63-
PySass_IF_PY3("yiyy", "siss"),
64-
&filename, &output_style,
65-
&include_paths, &image_path)) {
82+
PySass_IF_PY3("yiiyyO", "siissO"),
83+
&filename, &output_style, &source_comments,
84+
&include_paths, &image_path, &source_map_filename)) {
6685
return NULL;
6786
}
6887

6988
context = sass_new_file_context();
7089
context->input_path = filename;
90+
if (source_comments == SASS_SOURCE_COMMENTS_MAP &&
91+
PySass_Bytes_Check(source_map_filename)) {
92+
size_t source_map_file_len = PySass_Bytes_GET_SIZE(source_map_filename);
93+
if (source_map_file_len) {
94+
char *source_map_file = malloc(source_map_file_len + 1);
95+
strncpy(
96+
source_map_file,
97+
PySass_Bytes_AS_STRING(source_map_filename),
98+
source_map_file_len + 1
99+
);
100+
context->source_map_file = source_map_file;
101+
}
102+
}
71103
context->options.output_style = output_style;
104+
context->options.source_comments = source_comments;
72105
context->options.include_paths = include_paths;
73106
context->options.image_path = image_path;
74107

75108
sass_compile_file(context);
76109

110+
error_status = context->error_status;
77111
result = Py_BuildValue(
78-
PySass_IF_PY3("hy", "hs"),
112+
PySass_IF_PY3("hyy", "hss"),
79113
(short int) !context->error_status,
80-
context->error_status ? context->error_message : context->output_string
114+
error_status ? context->error_message : context->output_string,
115+
error_status || context->source_map_string == NULL
116+
? ""
117+
: context->source_map_string
81118
);
82119
sass_free_file_context(context);
83120
return result;
@@ -87,12 +124,13 @@ static PyObject *
87124
PySass_compile_dirname(PyObject *self, PyObject *args) {
88125
struct sass_folder_context *context;
89126
char *search_path, *output_path, *include_paths, *image_path;
90-
int output_style;
127+
int output_style, source_comments;
91128
PyObject *result;
92129

93130
if (!PyArg_ParseTuple(args,
94131
PySass_IF_PY3("yyiyy", "ssiss"),
95-
&search_path, &output_path, &output_style,
132+
&search_path, &output_path,
133+
&output_style, &source_comments,
96134
&include_paths, &image_path)) {
97135
return NULL;
98136
}
@@ -101,6 +139,7 @@ PySass_compile_dirname(PyObject *self, PyObject *args) {
101139
context->search_path = search_path;
102140
context->output_path = output_path;
103141
context->options.output_style = output_style;
142+
context->options.source_comments = source_comments;
104143
context->options.include_paths = include_paths;
105144
context->options.image_path = image_path;
106145

@@ -127,18 +166,25 @@ static PyMethodDef PySass_methods[] = {
127166

128167
static char PySass_doc[] = "The thin binding of libsass for Python.";
129168

130-
void PySass_init_module(PyObject *module) {
131-
PyObject *output_styles;
132-
size_t i = 0;
133-
output_styles = PyDict_New();
134-
for (i = 0; PySass_output_style_enum[i].label; ++i) {
169+
void PySass_make_enum_dict(PyObject *enum_dict, struct PySass_Pair *pairs) {
170+
size_t i;
171+
for (i = 0; pairs[i].label; ++i) {
135172
PyDict_SetItemString(
136-
output_styles,
137-
PySass_output_style_enum[i].label,
138-
PySass_Int_FromLong((long) PySass_output_style_enum[i].value)
173+
enum_dict,
174+
pairs[i].label,
175+
PySass_Int_FromLong((long) pairs[i].value)
139176
);
140177
}
178+
}
179+
180+
void PySass_init_module(PyObject *module) {
181+
PyObject *output_styles, *source_comments;
182+
output_styles = PyDict_New();
183+
PySass_make_enum_dict(output_styles, PySass_output_style_enum);
141184
PyModule_AddObject(module, "OUTPUT_STYLES", output_styles);
185+
source_comments = PyDict_New();
186+
PySass_make_enum_dict(source_comments, PySass_source_comments_enum);
187+
PyModule_AddObject(module, "SOURCE_COMMENTS", source_comments);
142188
}
143189

144190
#if PY_MAJOR_VERSION >= 3

sass.py

Lines changed: 92 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -16,17 +16,24 @@
1616

1717
from six import string_types, text_type
1818

19-
from _sass import (OUTPUT_STYLES, compile_dirname, compile_filename,
20-
compile_string)
19+
from _sass import (OUTPUT_STYLES, SOURCE_COMMENTS, compile_dirname,
20+
compile_filename, compile_string)
2121

22-
__all__ = 'MODES', 'OUTPUT_STYLES', 'CompileError', 'and_join', 'compile'
22+
__all__ = ('MODES', 'OUTPUT_STYLES', 'SOURCE_COMMENTS', 'CompileError',
23+
'and_join', 'compile')
2324
__version__ = '0.4.0'
2425

2526

2627
#: (:class:`collections.Mapping`) The dictionary of output styles.
2728
#: Keys are output name strings, and values are flag integers.
2829
OUTPUT_STYLES = OUTPUT_STYLES
2930

31+
#: (:class:`collections.Mapping`) The dictionary of source comments styles.
32+
#: Keys are mode names, and values are corresponding flag integers.
33+
#:
34+
#: .. versionadded:: 0.4.0
35+
SOURCE_COMMENTS = SOURCE_COMMENTS
36+
3037
#: (:class:`collections.Set`) The set of keywords :func:`compile()` can take.
3138
MODES = set(['string', 'filename', 'dirname'])
3239

@@ -53,6 +60,11 @@ def compile(**kwargs):
5360
choose one of: ``'nested'`` (default), ``'expanded'``,
5461
``'compact'``, ``'compressed'``
5562
:type output_style: :class:`str`
63+
:param source_comments: an optional source comments mode of the compiled
64+
result. choose one of ``'none'`` (default) or
65+
``'line_numbers'``. ``'map'`` is unavailable for
66+
``string``
67+
:type source_comments: :class:`str`
5668
:param include_paths: an optional list of paths to find ``@import``\ ed
5769
SASS/CSS source files
5870
:type include_paths: :class:`collections.Sequence`, :class:`str`
@@ -73,13 +85,28 @@ def compile(**kwargs):
7385
choose one of: ``'nested'`` (default), ``'expanded'``,
7486
``'compact'``, ``'compressed'``
7587
:type output_style: :class:`str`
88+
:param source_comments: an optional source comments mode of the compiled
89+
result. choose one of ``'none'`` (default),
90+
``'line_numbers'``, ``'map'``.
91+
if ``'map'`` is used it requires
92+
``source_map_filename`` argument as well and
93+
returns a (compiled CSS string,
94+
source map string) pair instead of a string
95+
:type source_comments: :class:`str`
96+
:param source_map_filename: indicate the source map output filename.
97+
it's only available and required
98+
when ``source_comments`` is ``'map'``.
99+
note that it will ignore all other parts of
100+
the path except for its basename
101+
:type source_map_filename: :class:`str`
76102
:param include_paths: an optional list of paths to find ``@import``\ ed
77103
SASS/CSS source files
78104
:type include_paths: :class:`collections.Sequence`, :class:`str`
79105
:param image_path: an optional path to find images
80106
:type image_path: :class:`str`
81-
:returns: the compiled CSS string
82-
:rtype: :class:`str`
107+
:returns: the compiled CSS string, or a pair of the compiled CSS string
108+
and the source map string if ``source_comments='map'``
109+
:rtype: :class:`str`, :class:`tuple`
83110
:raises sass.CompileError: when it fails for any reason
84111
(for example the given SASS has broken syntax)
85112
:raises exceptions.IOError: when the ``filename`` doesn't exist or
@@ -101,6 +128,11 @@ def compile(**kwargs):
101128
choose one of: ``'nested'`` (default), ``'expanded'``,
102129
``'compact'``, ``'compressed'``
103130
:type output_style: :class:`str`
131+
:param source_comments: an optional source comments mode of the compiled
132+
result. choose one of ``'none'`` (default) or
133+
``'line_numbers'``. ``'map'`` is unavailable for
134+
``dirname``
135+
:type source_comments: :class:`str`
104136
:param include_paths: an optional list of paths to find ``@import``\ ed
105137
SASS/CSS source files
106138
:type include_paths: :class:`collections.Sequence`, :class:`str`
@@ -109,6 +141,9 @@ def compile(**kwargs):
109141
:raises sass.CompileError: when it fails for any reason
110142
(for example the given SASS has broken syntax)
111143
144+
.. versionadded:: 0.4.0
145+
Added ``source_comments`` and ``source_map_filename`` parameters.
146+
112147
"""
113148
modes = set()
114149
for mode_name in MODES:
@@ -119,10 +154,7 @@ def compile(**kwargs):
119154
elif len(modes) > 1:
120155
raise TypeError(and_join(modes) + ' are exclusive each other; '
121156
'cannot be used at a time')
122-
try:
123-
output_style = kwargs.pop('output_style')
124-
except KeyError:
125-
output_style = 'nested'
157+
output_style = kwargs.pop('output_style', 'nested')
126158
if not isinstance(output_style, string_types):
127159
raise TypeError('output_style must be a string, not ' +
128160
repr(output_style))
@@ -131,7 +163,38 @@ def compile(**kwargs):
131163
except KeyError:
132164
raise CompileError('{0} is unsupported output_style; choose one of {1}'
133165
''.format(output_style, and_join(OUTPUT_STYLES)))
166+
source_comments = kwargs.pop('source_comments', 'none')
167+
if not isinstance(source_comments, string_types):
168+
raise TypeError('source_comments must be a string, not ' +
169+
repr(source_comments))
170+
if 'filename' not in modes and source_comments == 'map':
171+
raise CompileError('source_comments="map" is only available with '
172+
'filename= keyword argument since it has to be '
173+
'aware of it')
174+
try:
175+
source_comments = SOURCE_COMMENTS[source_comments]
176+
except KeyError:
177+
raise CompileError(
178+
'{0} is unsupported source_comments; choose one of '
179+
'{1}'.format(source_comments, and_join(SOURCE_COMMENTS))
180+
)
134181
fs_encoding = sys.getfilesystemencoding() or sys.getdefaultencoding()
182+
try:
183+
source_map_filename = kwargs.pop('source_map_filename') or b''
184+
except KeyError:
185+
if source_comments == SOURCE_COMMENTS['map']:
186+
raise TypeError('source_comments="map" requires '
187+
'source_map_filename argument')
188+
source_map_filename = b''
189+
else:
190+
if source_comments != SOURCE_COMMENTS['map']:
191+
raise TypeError('source_map_filename is available only with '
192+
'source_comments="map"')
193+
elif not isinstance(source_map_filename, string_types):
194+
raise TypeError('source_map_filename must be a string, not ' +
195+
repr(source_map_filename))
196+
if isinstance(source_map_filename, text_type):
197+
source_map_filename = source_map_filename.encode(fs_encoding)
135198
try:
136199
include_paths = kwargs.pop('include_paths') or b''
137200
except KeyError:
@@ -159,7 +222,11 @@ def compile(**kwargs):
159222
string = kwargs.pop('string')
160223
if isinstance(string, text_type):
161224
string = string.encode('utf-8')
162-
s, v = compile_string(string, output_style, include_paths, image_path)
225+
s, v = compile_string(string,
226+
output_style, source_comments,
227+
include_paths, image_path)
228+
if s:
229+
return v.decode('utf-8')
163230
elif 'filename' in modes:
164231
filename = kwargs.pop('filename')
165232
if not isinstance(filename, string_types):
@@ -168,8 +235,16 @@ def compile(**kwargs):
168235
raise IOError('{0!r} seems not a file'.format(filename))
169236
elif isinstance(filename, text_type):
170237
filename = filename.encode(fs_encoding)
171-
s, v = compile_filename(filename,
172-
output_style, include_paths, image_path)
238+
s, v, source_map = compile_filename(
239+
filename,
240+
output_style, source_comments,
241+
include_paths, image_path, source_map_filename
242+
)
243+
if s:
244+
v = v.decode('utf-8')
245+
if source_map_filename:
246+
v = v, source_map.decode('utf-8')
247+
return v
173248
elif 'dirname' in modes:
174249
try:
175250
search_path, output_path = kwargs.pop('dirname')
@@ -182,11 +257,13 @@ def compile(**kwargs):
182257
if isinstance(output_path, text_type):
183258
output_path = output_path.encode(fs_encoding)
184259
s, v = compile_dirname(search_path, output_path,
185-
output_style, include_paths, image_path)
260+
output_style, source_comments,
261+
include_paths, image_path)
262+
if s:
263+
return
186264
else:
187265
raise TypeError('something went wrong')
188-
if s:
189-
return None if v is None else v.decode('utf-8')
266+
assert not s
190267
raise CompileError(v)
191268

192269

0 commit comments

Comments
 (0)